Merge remote-tracking branch 'origin/master' into feature/eslint-typescript-flat
@@ -23,6 +23,10 @@
|
||||
"prConcurrentLimit": 20,
|
||||
"prHourlyLimit": 10,
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": [
|
||||
"dependencies",
|
||||
"security"
|
||||
],
|
||||
"prHourlyLimit": 0,
|
||||
"prConcurrentLimit": 0
|
||||
},
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: 'Tools: Check Test Init Files'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-test-init-files:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for __init__.py files in test directories
|
||||
run: python3 scripts/check_test_init_files.py .
|
||||
@@ -14,19 +14,19 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
sdk_version:
|
||||
description: 'SDK version override (empty = auto-derive from prowler/CHANGELOG.md + pending fragment types; "skip" = hold this component back)'
|
||||
description: 'SDK version override (empty = mirrors prowler_version; "skip" = hold this component back)'
|
||||
required: false
|
||||
type: string
|
||||
api_version:
|
||||
description: 'API version override (empty = auto-derive; "skip" = hold back)'
|
||||
description: 'API version override (empty = auto-derive 1.<prowler_minor + 1>.<prowler_patch>; "skip" = hold back)'
|
||||
required: false
|
||||
type: string
|
||||
ui_version:
|
||||
description: 'UI version override (empty = auto-derive; "skip" = hold back)'
|
||||
description: 'UI version override (empty = auto-derive 1.<prowler_minor>.<prowler_patch>; "skip" = hold back)'
|
||||
required: false
|
||||
type: string
|
||||
mcp_version:
|
||||
description: 'MCP Server version override (empty = auto-derive; "skip" = hold back)'
|
||||
description: 'MCP Server version override (empty = auto-derive from pending fragment types; "skip" = hold back)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -209,17 +209,48 @@ jobs:
|
||||
errors=1
|
||||
continue
|
||||
fi
|
||||
IFS=. read -r major minor patch <<< "$current"
|
||||
major=$((10#$major))
|
||||
minor=$((10#$minor))
|
||||
patch=$((10#$patch))
|
||||
if echo "$fragments" | grep -qE '\.(added|changed|deprecated)(\.[0-9]+)?\.md$'; then
|
||||
effective="${major}.$((minor + 1)).0"
|
||||
else
|
||||
effective="${major}.${minor}.$((patch + 1))"
|
||||
# SDK, UI, and API versions are deterministic mirrors of the
|
||||
# Prowler version (the scheme bump-version.yml codifies): the SDK
|
||||
# mirrors it directly, the UI tracks 1.<minor>.<patch>, and the
|
||||
# API is the independent 1.<minor + 1>.<patch> stream. Only the
|
||||
# MCP Server has its own cadence, derived from fragment types.
|
||||
IFS=. read -r _ prowler_minor prowler_patch <<< "$PROWLER_VERSION"
|
||||
prowler_minor=$((10#$prowler_minor))
|
||||
prowler_patch=$((10#$prowler_patch))
|
||||
case "$component" in
|
||||
prowler) effective="$PROWLER_VERSION" ;;
|
||||
ui) effective="1.${prowler_minor}.${prowler_patch}" ;;
|
||||
api) effective="1.$((prowler_minor + 1)).${prowler_patch}" ;;
|
||||
mcp_server)
|
||||
IFS=. read -r major minor patch <<< "$current"
|
||||
major=$((10#$major))
|
||||
minor=$((10#$minor))
|
||||
patch=$((10#$patch))
|
||||
# Prowler patch releases (vN.N target) are maintenance
|
||||
# releases, so the MCP Server bumps patch regardless of
|
||||
# fragment types; a deliberate exception needs the explicit
|
||||
# version input.
|
||||
if [ "$TARGET_BRANCH" != "master" ]; then
|
||||
effective="${major}.${minor}.$((patch + 1))"
|
||||
if echo "$fragments" | grep -qE '\.(added|deprecated)(\.[0-9]+)?\.md$'; then
|
||||
echo "::warning::${component}: 'added'/'deprecated' fragments are shipping in a Prowler patch; auto-derived a patch bump (${current} -> ${effective}), pass the version input to override"
|
||||
fi
|
||||
elif echo "$fragments" | grep -qE '\.(added|changed|deprecated)(\.[0-9]+)?\.md$'; then
|
||||
effective="${major}.$((minor + 1)).0"
|
||||
else
|
||||
effective="${major}.${minor}.$((patch + 1))"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
current_key=$(version_key "$current")
|
||||
effective_key=$(version_key "$effective")
|
||||
if [[ "$effective_key" < "$current_key" || "$effective_key" == "$current_key" ]]; then
|
||||
echo "::error::${component}: auto-derived version '${effective}' is not greater than the latest released version (${current}); check prowler_version or pass the version input explicitly"
|
||||
errors=1
|
||||
continue
|
||||
fi
|
||||
mode="auto"
|
||||
echo "::notice::${component}: version auto-derived ${current} -> ${effective} from the pending fragment types"
|
||||
echo "::notice::${component}: version auto-derived ${current} -> ${effective}"
|
||||
fi
|
||||
|
||||
if [ "$removed_fragments" = "true" ]; then
|
||||
@@ -336,6 +367,7 @@ jobs:
|
||||
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
|
||||
labels: |
|
||||
no-changelog
|
||||
skip-sync
|
||||
|
||||
# Patch compiles (target_branch = v5.X) leave master holding the consumed
|
||||
# fragments and missing the new version block. This applies the equivalent
|
||||
@@ -366,7 +398,7 @@ jobs:
|
||||
local block_file="$2"
|
||||
local changelog="${component}/CHANGELOG.md"
|
||||
local incoming_heading incoming_release incoming_key
|
||||
local marker_line insertion_line duplicate_line
|
||||
local marker_line insertion_line duplicate_line total_lines
|
||||
local line heading existing_release existing_key
|
||||
|
||||
marker_line=$(grep -n -m1 '^<!-- changelog: release notes start -->$' "$changelog" | cut -d: -f1)
|
||||
@@ -405,9 +437,25 @@ jobs:
|
||||
insertion_line=$((marker_line + 1))
|
||||
fi
|
||||
|
||||
# The captured block window can be off by one blank line on either
|
||||
# end (towncrier re-emits the blank after the marker), so strip the
|
||||
# outer blank lines and pad exactly one on each side: the block
|
||||
# must never glue to the marker above or the next heading below.
|
||||
awk '
|
||||
/[^[:space:]]/ { for (i = 0; i < pending; i++) print ""; pending = 0; print; started = 1; next }
|
||||
started { pending++ }
|
||||
' "$block_file" > "${RUNNER_TEMP}/block-normalized.md"
|
||||
|
||||
total_lines=$(wc -l < "$changelog")
|
||||
{
|
||||
head -n "$((insertion_line - 1))" "$changelog"
|
||||
cat "$block_file"
|
||||
if [ "$insertion_line" -gt 1 ] && [ -n "$(sed -n "$((insertion_line - 1))p" "$changelog")" ]; then
|
||||
echo ""
|
||||
fi
|
||||
cat "${RUNNER_TEMP}/block-normalized.md"
|
||||
if [ "$insertion_line" -le "$total_lines" ]; then
|
||||
echo ""
|
||||
fi
|
||||
tail -n +"$insertion_line" "$changelog"
|
||||
} > "${RUNNER_TEMP}/changelog.tmp"
|
||||
mv "${RUNNER_TEMP}/changelog.tmp" "$changelog"
|
||||
@@ -476,3 +524,4 @@ jobs:
|
||||
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
|
||||
labels: |
|
||||
no-changelog
|
||||
skip-sync
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Add community label
|
||||
if: steps.check_membership.outputs.is_member == 'false'
|
||||
if: steps.check_membership.outputs.is_member == 'false' && github.event.pull_request.user.type != 'Bot'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -5,6 +5,8 @@ LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
ARG POWERSHELL_VERSION=7.5.0
|
||||
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
|
||||
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
|
||||
ENV POWERSHELL_TELEMETRY_OPTOUT=1
|
||||
|
||||
ARG TRIVY_VERSION=0.71.2
|
||||
ENV TRIVY_VERSION=${TRIVY_VERSION}
|
||||
|
||||
@@ -4,7 +4,6 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
|
||||
## [1.34.1] (Prowler v5.33.1)
|
||||
|
||||
### 🐞 Fixed
|
||||
@@ -19,6 +18,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
- `LIGHTHOUSE_AI_OPENAI_COMPATIBLE_ALLOWED_HOSTS` environment variable to allow internal hosts as OpenAI-compatible Lighthouse AI base URLs [(#11942)](https://github.com/prowler-cloud/prowler/pull/11942)
|
||||
|
||||
---
|
||||
|
||||
## [1.34.0] (Prowler v5.33.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"user-guide/tutorials/prowler-alerts",
|
||||
"user-guide/tutorials/prowler-app-scan-configuration",
|
||||
"user-guide/tutorials/prowler-app-findings-triage",
|
||||
"user-guide/compliance/tutorials/cross-provider-compliance",
|
||||
{
|
||||
"group": "Mutelist",
|
||||
"expanded": true,
|
||||
|
||||
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 372 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 353 KiB |
@@ -262,6 +262,7 @@ To request a new framework or contribute one, see [Creating a New Security Compl
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance)
|
||||
* [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore)
|
||||
* [Creating a New Security Compliance Framework in Prowler](/developer-guide/security-compliance-framework)
|
||||
* [Prowler App — Getting Started](/user-guide/tutorials/prowler-app)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: 'Cross-Provider Compliance'
|
||||
description: 'Aggregate a single universal compliance framework across every connected provider in Prowler Cloud, review a consolidated roll-up and per-provider breakdown, and download a combined PDF report.'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
|
||||
|
||||
<VersionBadge version="5.34.0" />
|
||||
|
||||
Cross-Provider Compliance consolidates a single **universal compliance framework** across all of your connected providers into one unified view. Instead of reviewing the same framework on AWS, Azure, Google Cloud, and every other provider as separate reports, Prowler takes the most recent completed scan of every compatible provider, aggregates them by requirement, and produces a single roll-up posture with a per-provider breakdown and a combined executive PDF.
|
||||
|
||||
<SubscriptionBanner />
|
||||
|
||||
## What Is a Universal Compliance Framework
|
||||
|
||||
Most Prowler compliance frameworks target a single provider (for example, CIS AWS or CIS Azure). A **universal** framework declares its requirements once in a single JSON and maps each requirement to checks for many providers at the same time, so the same CSA CCM control maps to both AWS and Azure checks. Universal frameworks live at the top of the compliance catalog (`prowler/compliance/<framework>.json`), as opposed to the legacy per-provider frameworks under `prowler/compliance/<provider>/`. For real definitions, see [`csa_ccm_4.0.json`](https://github.com/prowler-cloud/prowler/blob/master/prowler/compliance/csa_ccm_4.0.json), [`cis_controls_8.1.json`](https://github.com/prowler-cloud/prowler/blob/master/prowler/compliance/cis_controls_8.1.json), and [`dora_2022_2554.json`](https://github.com/prowler-cloud/prowler/blob/master/prowler/compliance/dora_2022_2554.json) in the Prowler repository.
|
||||
|
||||
Prowler currently ships three universal frameworks: [CSA CCM](https://hub.prowler.com/compliance/csa_ccm_4.0), [CIS Controls](https://hub.prowler.com/compliance/cis_controls_8.1), and [DORA](https://hub.prowler.com/compliance/dora_2022_2554). Each framework page on Prowler Hub lists the full requirement-to-check mapping per provider.
|
||||
|
||||
## How Cross-Provider Compliance Works
|
||||
|
||||
Cross-Provider Compliance uses this structure to answer a single question: **"How compliant is my whole estate against this framework, regardless of provider?"** For a chosen universal framework, Prowler Cloud:
|
||||
|
||||
1. Selects **one scan per compatible provider**: by default, the latest completed scan of each provider the framework supports and that you are allowed to see.
|
||||
2. Aggregates the requirement results across those scans, folding multiple accounts of the same provider type together.
|
||||
3. Computes a **roll-up status** for each requirement and an overall pass / fail / manual summary for the framework.
|
||||
4. Exposes a **per-provider breakdown** so you can see exactly which provider is failing a given control.
|
||||
|
||||
<Note>
|
||||
Cross-Provider Compliance never mixes different frameworks. It aggregates one universal framework at a time across providers. To review a single provider in isolation, use the standard per-scan [Compliance](/user-guide/compliance/tutorials/compliance) view.
|
||||
</Note>
|
||||
|
||||
### Supported Universal Frameworks
|
||||
|
||||
The Cross-Provider Compliance view currently supports the following universal frameworks. The compatible providers are the ones each framework declares checks for; a provider only contributes to the roll-up when it has a completed scan.
|
||||
|
||||
| Framework | Version | Compatible providers |
|
||||
|-----------|---------|----------------------|
|
||||
| [**CSA CCM**](https://hub.prowler.com/compliance/csa_ccm_4.0) (Cloud Controls Matrix) | 4.0 | AWS, Azure, Google Cloud, Alibaba Cloud, Oracle Cloud |
|
||||
| [**CIS Controls**](https://hub.prowler.com/compliance/cis_controls_8.1) | 8.1 | AWS, Azure, Google Cloud, Microsoft 365, Kubernetes, GitHub, Google Workspace, Okta, Oracle Cloud, Alibaba Cloud, Cloudflare, MongoDB Atlas, OpenStack, Vercel |
|
||||
| [**DORA**](https://hub.prowler.com/compliance/dora_2022_2554) (Digital Operational Resilience Act) | 2022/2554 | AWS, Azure, Google Cloud, Alibaba Cloud, Cloudflare |
|
||||
|
||||
The catalog grows as new universal frameworks ship in Prowler. Browse the full compliance catalog at [Prowler Hub](https://hub.prowler.com/compliance).
|
||||
|
||||
## Accessing the Cross-Provider View
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the Compliance section">
|
||||
Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) and select **Compliance** from the left navigation.
|
||||
</Step>
|
||||
<Step title="Switch to the Cross-provider tab">
|
||||
At the top of the Compliance page, select the **Cross-provider** tab. The **Per Scan** tab (the default) keeps the single-provider compliance experience unchanged.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<img src="/images/compliance/prowler-app-cross-provider-tab.png" alt="Compliance page showing the Per Scan and Cross-provider tabs, with the Cross-provider tab highlighted" width="900" />
|
||||
|
||||
<Note>
|
||||
Cross-Provider Compliance requires at least one completed scan for a compatible provider. If no compatible provider has finished a scan yet, the page shows a notice prompting you to launch or wait for a scan to complete.
|
||||
</Note>
|
||||
|
||||
## Exploring the Overview
|
||||
|
||||
The overview presents one card per supported universal framework, each summarizing the consolidated posture across every contributing provider.
|
||||
|
||||
<img src="/images/compliance/prowler-app-cross-provider-overview.png" alt="Cross-Provider Compliance overview showing the provider filters and the framework grid (CSA CCM, CIS Controls, DORA) with per-provider chips, scores, and failed/manual counts" width="900" />
|
||||
|
||||
Each **framework card** includes:
|
||||
|
||||
* **Framework logo, name, and version:** Identifies the universal standard (CSA CCM, CIS Controls, DORA).
|
||||
* **Score:** The percentage of passing requirements over the total evaluated, aggregated across every contributing provider. Color coding follows three thresholds: red for severely low compliance, amber for partial compliance, and green for healthy posture.
|
||||
* **Passing Requirements:** A `passed / total` counter with the aggregated roll-up.
|
||||
* **Provider chips:** One icon per compatible provider. Providers with a completed scan appear active with their passing percentage; providers without a scan appear dimmed with a "no completed scan yet" tooltip so coverage gaps are obvious at a glance.
|
||||
* **Failed and manual counts:** The number of failing and manual requirements in the roll-up.
|
||||
|
||||
Select any card to open the framework detail page.
|
||||
|
||||
### Filtering the Roll-Up
|
||||
|
||||
The filters bar controls which providers and accounts feed every card and detail view. Cross-Provider Compliance supports three filters:
|
||||
|
||||
* **Provider type:** Narrow the roll-up to specific provider types (for example, only AWS and Azure).
|
||||
* **Provider / account:** Narrow to specific connected accounts.
|
||||
* **Provider group:** Narrow to accounts within one or more provider groups.
|
||||
|
||||
Select **Clear filters** to reset all filters. Filters applied on the overview are carried through into the detail page and the PDF report so the view stays consistent end to end.
|
||||
|
||||
<Note>
|
||||
Filters narrow **which providers contribute** to the aggregation. They do not change how a requirement rolls up (see [Understanding the Roll-Up Status](#understanding-the-roll-up-status)).
|
||||
</Note>
|
||||
|
||||
## Working With the Framework Detail Page
|
||||
|
||||
The detail page provides the full breakdown for a single universal framework: aggregate metrics, provider coverage, top failing sections, and a requirement-by-requirement view with per-provider status.
|
||||
|
||||
<img src="/images/compliance/prowler-app-cross-provider-detail.png" alt="Cross-Provider Compliance detail page for CSA CCM 4.0 showing the header with providers scanned and the Report button, the filters, and the Requirements Status, Provider Coverage, and Top Failed Sections summary cards" width="900" />
|
||||
|
||||
### Header
|
||||
|
||||
The header shows the framework name and version, a link to the framework page on [Prowler Hub](https://hub.prowler.com/compliance), and a summary such as *"X of Y compatible providers scanned · N scans aggregated"* so you always know the coverage behind the numbers. The **Report** button in the top-right generates and downloads the combined PDF (see [Downloading the Combined PDF Report](#downloading-the-combined-pdf-report)).
|
||||
|
||||
### Summary Cards
|
||||
|
||||
Below the header, three summary cards condense the framework state:
|
||||
|
||||
* **Requirements Status:** Donut chart with `Pass`, `Fail`, and `Manual` counts plus the total number of requirements, reflecting the consolidated roll-up.
|
||||
* **Provider Coverage:** Shows which compatible providers contributed a scan and their individual posture, so coverage gaps and per-provider weak spots are visible at a glance.
|
||||
* **Top Failed Sections:** Ranks the framework sections with the highest number of failing requirements, with deep links into the requirements accordion.
|
||||
|
||||
### Requirements Accordion
|
||||
|
||||
The accordion organizes every requirement of the framework. For each requirement you see:
|
||||
|
||||
* **Requirement ID and title:** The official identifier from the framework.
|
||||
* **Roll-up status badge:** A single `Pass`, `Fail`, or `Manual` badge representing the consolidated status across all contributing providers.
|
||||
* **Per-provider status:** The status each contributing provider returned for that requirement, so a single failing provider is immediately attributable.
|
||||
* **Provider-labeled checks:** When you expand a requirement, the underlying checks are labeled with the provider they belong to (each universal requirement maps to different check IDs per provider).
|
||||
|
||||
Expand a requirement to review the failing checks per provider, the affected resources, and remediation guidance. Findings are queried across every contributing scan and merged into a single table.
|
||||
|
||||
<img src="/images/compliance/prowler-app-cross-provider-requirements-accordion.png" alt="Expanded DORA requirement showing the per-provider Fail badges for AWS, Azure, and Google Cloud, the requirement description and attributes, the checks count, and the merged findings table with a Provider column" width="900" />
|
||||
|
||||
## Understanding the Roll-Up Status
|
||||
|
||||
Cross-Provider Compliance rolls up results in two stages, with a strict **FAIL > PASS > MANUAL** precedence.
|
||||
|
||||
**Per provider, per requirement:**
|
||||
|
||||
* If any check fails → the provider contributes **FAIL** for that requirement.
|
||||
* Else if every check passes → **PASS**.
|
||||
* Otherwise (no pass/fail evidence) → **MANUAL**.
|
||||
|
||||
Multiple accounts of the same provider type (for example, three AWS accounts) are folded together first, so a failure in any one account marks that provider type as failing.
|
||||
|
||||
**Across providers, per requirement (the roll-up badge):**
|
||||
|
||||
* If at least one contributing provider is **FAIL** → the requirement is **FAIL**.
|
||||
* Else if at least one contributing provider is **PASS** → **PASS**.
|
||||
* Otherwise → **MANUAL**.
|
||||
|
||||
<Note>
|
||||
Only providers that **actually contributed a result** for a requirement are counted. A provider that has a scan in the aggregation but produced no result for a specific requirement (for example, because the framework maps no checks to that provider for that control) does **not** degrade the requirement to Manual. This keeps the roll-up focused on real evidence.
|
||||
</Note>
|
||||
|
||||
### How Scans Are Selected
|
||||
|
||||
By default, Cross-Provider Compliance auto-selects the **latest completed scan** of each compatible provider you are allowed to see. This means:
|
||||
|
||||
* The view always reflects your most recent posture per provider, without any manual scan selection.
|
||||
* Adding a new compatible provider and running a scan automatically brings it into the roll-up.
|
||||
* Provider visibility follows your role: you only ever see providers your permissions allow, and the roll-up is scoped accordingly.
|
||||
|
||||
## Downloading the Combined PDF Report
|
||||
|
||||
The **Report** button on the detail page generates a single PDF that combines every contributing provider's latest scan for the framework into one executive document: a cover page listing all providers and accounts, an executive summary with the consolidated roll-up, charts, a requirements index, and detailed findings grouped by requirement, provider, and account.
|
||||
|
||||
### The Report Follows Your Filters
|
||||
|
||||
A report covers the exact set of scans your current filters resolve to. The provider type, provider / account, and provider group filters applied on the detail page determine which providers contribute, and the auto-select rule pins each contributing provider's latest completed scan. The PDF is built from that resolved scan set together with the framework.
|
||||
|
||||
As a result, each filter combination produces its own report. For example:
|
||||
|
||||
* No filters → a report covering every compatible provider that has a completed scan.
|
||||
* `Provider type = AWS, Azure` → a report covering only your AWS and Azure scans.
|
||||
* `Provider group = Production` → a report covering only the accounts in that group.
|
||||
|
||||
Changing the filters and generating again produces a different, independent report. Each combination is tracked on its own, so switching filters back and forth never overwrites a previously generated report.
|
||||
|
||||
<Note>
|
||||
Region filtering is **not** supported for the combined PDF report. The report recomputes status live across every region of the contributing scans, so a region-scoped request is rejected rather than producing a report that contradicts a region-filtered view.
|
||||
</Note>
|
||||
|
||||
### Generating and Reusing a Report
|
||||
|
||||
Because the report aggregates many scans, it is generated **asynchronously**:
|
||||
|
||||
<Steps>
|
||||
<Step title="Start the report">
|
||||
Select **Report → Generate new report…**, optionally give it a name, and confirm. Prowler starts a background job and shows a "Report generation started" confirmation.
|
||||
</Step>
|
||||
<Step title="Wait for completion">
|
||||
The button shows a "Generating report…" state while the job runs. Generation continues in the background: you can navigate away, and a toast notification appears when the report is ready, even after a page reload.
|
||||
</Step>
|
||||
<Step title="Download">
|
||||
When the report is ready, select **Download** from the notification, or use **Report → Download latest** at any time to fetch the most recent report for the current filters.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<img src="/images/compliance/prowler-app-cross-provider-report.png" alt="Report dropdown on the Cross-Provider Compliance detail page showing the Generate new report option" width="420" />
|
||||
|
||||
A report already generated for a given set of filters does not need to be generated again. When you open the detail page with a filter combination that was reported before, Prowler detects the existing report and surfaces **Report → Download latest** so you can download it immediately, without launching a new job. You only need to generate a fresh report when:
|
||||
|
||||
* You apply a filter combination that has never been reported before, or
|
||||
* A contributing provider has run a new scan since the report was generated. The report is tied to the specific scans it was built from, so a newer scan makes the previous report stale; Prowler recognizes it no longer matches the current selection and offers to generate an up-to-date one.
|
||||
|
||||
"Download latest" reuses an existing report only when it matches the framework, the exact resolved scan set for the current filters, and the same report options. This guarantees the PDF you download reflects the posture you are looking at, rather than a report generated for a different filter or an older scan.
|
||||
|
||||
<Note>
|
||||
The PDF detail section renders only **failed** requirements by default so the report stays focused as an executive/auditor document. As with every Prowler PDF, the detail section is capped at the first 100 failed findings per check; use the per-scan CSV or JSON-OCSF exports for the complete, untruncated list. See [Downloading Compliance Reports](/user-guide/compliance/tutorials/compliance#downloading-compliance-reports) for the full PDF behavior and the `DJANGO_PDF_MAX_FINDINGS_PER_CHECK` setting.
|
||||
</Note>
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Compliance](/user-guide/compliance/tutorials/compliance)
|
||||
* [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore)
|
||||
* [Creating a New Security Compliance Framework in Prowler](/developer-guide/security-compliance-framework)
|
||||
* [Prowler App — Getting Started](/user-guide/tutorials/prowler-app)
|
||||
@@ -2,18 +2,16 @@
|
||||
title: "Prowler ThreatScore Documentation"
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
The **Prowler ThreatScore** is a comprehensive compliance scoring system that provides a unified metric for assessing your organization's security posture across compliance frameworks. It aggregates findings from individual security checks into a single, normalized score ranging from 0 to 100.
|
||||
Prowler ThreatScore is a comprehensive compliance scoring system that provides a unified metric for assessing security posture across compliance frameworks. It aggregates findings from individual security checks into a single, normalized score ranging from 0 to 100.
|
||||
|
||||
### Purpose
|
||||
- **Unified View**: Get a single metric representing overall compliance health
|
||||
- **Risk Prioritization**: Understand which areas pose the highest security risks
|
||||
- **Progress Tracking**: Monitor improvements in compliance posture over time
|
||||
- **Executive Reporting**: Provide clear, quantifiable security metrics to stakeholders
|
||||
|
||||
- **Unified view:** A single metric represents overall compliance health.
|
||||
- **Risk prioritization:** Highlights which areas pose the highest security risks.
|
||||
- **Progress tracking:** Monitors improvements in compliance posture over time.
|
||||
- **Executive reporting:** Delivers clear, quantifiable security metrics to stakeholders.
|
||||
|
||||
## How ThreatScore Works
|
||||
|
||||
@@ -29,7 +27,7 @@ Pass Rate = (Number of PASS findings) / (Total findings)
|
||||
The total number of checks performed (both PASS and FAIL) for a requirement. This represents the amount of evidence available - more findings provide greater confidence in the assessment.
|
||||
|
||||
### 3. Weight (`weight_i`)
|
||||
A numerical value (1-1000) representing the business importance or criticality of the requirement within your organization's context.
|
||||
A numerical value (1-1000) representing the business importance or criticality of the requirement within the organizational context.
|
||||
|
||||
### 4. Risk Level (`risk_i`)
|
||||
A severity rating (1-5) indicating the potential impact of non-compliance with this requirement.
|
||||
@@ -380,7 +378,7 @@ This comprehensive example demonstrates how:
|
||||
|
||||
### Example 4: Impact of Parameter Changes
|
||||
|
||||
Using the scenario, let's see how parameter changes affect the score:
|
||||
Using the scenario, the following changes show how parameter adjustments affect the score:
|
||||
|
||||
#### Scenario A: Increase Encryption Risk Level
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ To resend the invitation to the user, it is necessary to explicitly **delete the
|
||||
|
||||
## Managing Groups and Roles
|
||||
|
||||
The Roles section in Prowler is designed to facilitate the assignment of custom user privileges. This section allows administrators to define roles with specific permissions for Prowler administrative tasks and Account visibility.
|
||||
Roles combine administrative permissions with provider visibility. Administrative permissions control the actions a role can perform. Provider Groups and Unlimited Visibility control the providers, resources, findings, scans, and compliance results the role can access.
|
||||
|
||||
<Note>
|
||||
**Only users that have the _Manage Account_ or _admin_ permission can access this section.**
|
||||
@@ -132,47 +132,53 @@ The Roles section in Prowler is designed to facilitate the assignment of custom
|
||||
</Note>
|
||||
### Provider Groups
|
||||
|
||||
Provider Groups control visibility across specific providers. When creating a new role, you can assign specific groups to define their Provider visibility. This ensures that users with that role have access only to the Providers that are required.
|
||||
Provider Groups limit visibility to selected providers. Assigning one or more Provider Groups to a role grants access to the providers in those groups and their resources, findings, scans, and compliance results.
|
||||
|
||||
By default, a new user role does not have visibility into any group.
|
||||
New roles have no provider visibility by default. Assign at least one Provider Group or enable **Unlimited Visibility** before assigning the role to users who need access to provider data.
|
||||
|
||||
Alternatively, to grant the role unlimited visibility across all providers, check the Grant Unlimited Visibility checkbox.
|
||||
**Unlimited Visibility** grants organization-wide visibility across every provider, regardless of the Provider Groups assigned to the role. It does not grant administrative permissions.
|
||||
|
||||
#### Creating a Provider Group
|
||||
|
||||
Follow these steps to create a provider group in your account:
|
||||
|
||||
1. Navigate to **Provider Groups** from the side menu..
|
||||
1. Click **Providers** in the side menu.
|
||||
|
||||
2. In this view you can select the provider groups you want to assign to one or more roles.
|
||||
2. Select the **Provider Groups** tab.
|
||||
|
||||
3. Click the **Create Group** button on the center of the screen.
|
||||
3. Enter a group name in the **Create a new provider group** form.
|
||||
|
||||
<img src="/images/prowler-app/rbac/provider_group.png" alt="Create Provider Group" width="700" />
|
||||
4. Select the providers that the group controls. Optionally, select the roles that should use the group.
|
||||
|
||||
5. Click **Create Group**.
|
||||
|
||||
<img src="/images/prowler-app/rbac/provider_group.png" alt="Create a Provider Group" width="700" />
|
||||
|
||||
#### Editing a Provider Group
|
||||
|
||||
Follow these steps to edit a provider group on your account:
|
||||
|
||||
1. Navigate to **Provider Groups** from the side menu.
|
||||
1. Click **Providers** in the side menu and select the **Provider Groups** tab.
|
||||
|
||||
2. Click the edit button of the provider group you want to modify.
|
||||
2. Open the actions menu for the Provider Group and click **Edit Provider Group**.
|
||||
|
||||
<img src="/images/prowler-app/rbac/provider_group_edit.png" alt="Edit Provider Group" width="700" />
|
||||
<img src="/images/prowler-app/rbac/provider_group_edit.png" alt="Edit Provider Group action" width="300" />
|
||||
|
||||
3. Change the provider group parameters you need and save the changes.
|
||||
3. Update the group name, providers, or roles, and save the changes.
|
||||
|
||||
<img src="/images/prowler-app/rbac/provider_group_edit_1.png" alt="Edit Provider Group Details" width="700" />
|
||||
<img src="/images/prowler-app/rbac/provider_group_edit_1.png" alt="Edit Provider Group form" width="700" />
|
||||
|
||||
#### Removing a Provider Group
|
||||
|
||||
Follow these steps to remove a provider group of your account:
|
||||
Follow these steps to remove a provider group from your account:
|
||||
|
||||
1. Navigate to **Provider Groups** from the side menu.
|
||||
1. Click **Providers** in the side menu and select the **Provider Groups** tab.
|
||||
|
||||
2. Click the delete button of the provider group you want to remove.
|
||||
2. Open the actions menu for the Provider Group and click **Delete Provider Group**.
|
||||
|
||||
<img src="/images/prowler-app/rbac/provider_group_remove.png" alt="Remove Provider Group" width="700" />
|
||||
3. Confirm the deletion.
|
||||
|
||||
<img src="/images/prowler-app/rbac/provider_group_remove.png" alt="Delete Provider Group confirmation" width="700" />
|
||||
|
||||
### Roles
|
||||
|
||||
@@ -182,19 +188,20 @@ Follow these steps to create a role for your account:
|
||||
|
||||
1. Navigate to **Roles** from the side menu.
|
||||
|
||||
2. Click the **Add Role** button on the top right-hand corner of the screen.
|
||||
2. Click **Add Role**.
|
||||
|
||||
<img src="/images/prowler-app/rbac/role_create.png" alt="Create Role" width="700" />
|
||||
3. Enter the role name and select the required administrative permissions.
|
||||
|
||||
3. In the Add Role screen, enter the role name, the administration permissions and the groups of providers to which the Role will have access to.
|
||||
|
||||
4. In the Groups and Account Visibility section, you will see a list of available groups with checkboxes next to them. To assign a group to the user role, simply click the checkbox next to the group name. If you need to assign multiple groups, repeat the process for each group you wish to add.
|
||||
4. Configure **Visibility**:
|
||||
- To grant organization-wide visibility, select **Enable Unlimited Visibility for this role**.
|
||||
- To limit visibility, leave Unlimited Visibility cleared and select one or more Provider Groups.
|
||||
|
||||
<img src="/images/prowler-app/rbac/role_create_1.png" alt="Role parameters" width="700" />
|
||||
|
||||
5. Click **Add Role**.
|
||||
|
||||
<Note>
|
||||
To assign read-only access, select only the `Unlimited Visibility` permission when creating the role. Then, go to the Users page and assign this role to the appropriate user.
|
||||
To grant read-only access across the organization, enable **Unlimited Visibility** without selecting administrative permissions. Then, assign the role from the **Users** page.
|
||||
|
||||
</Note>
|
||||
#### Editing a Role
|
||||
@@ -203,25 +210,21 @@ Follow these steps to edit a role on your account:
|
||||
|
||||
1. Navigate to **Roles** from the side menu.
|
||||
|
||||
2. Click the edit button of the role you want to modify.
|
||||
2. Open the actions menu for the role and click **Edit Role**.
|
||||
|
||||
<img src="/images/prowler-app/rbac/role_edit.png" alt="Edit Role" width="700" />
|
||||
|
||||
3. Adjust the settings as needed and save the changes.
|
||||
|
||||
<img src="/images/prowler-app/rbac/role_edit_details.png" alt="Edit Role Details" width="700" />
|
||||
3. Update the role name, administrative permissions, Unlimited Visibility setting, or Provider Groups.
|
||||
|
||||
4. Save the changes.
|
||||
|
||||
#### Removing a Role
|
||||
|
||||
Follow these steps to remove a role of your account:
|
||||
Follow these steps to remove a role from your account:
|
||||
|
||||
1. Navigate to **Roles** from the side menu.
|
||||
|
||||
2. Click the delete button of the role you want to remove.
|
||||
|
||||
<img src="/images/prowler-app/rbac/role_remove.png" alt="Remove Role" width="700" />
|
||||
2. Open the actions menu for the role and click **Delete Role**.
|
||||
|
||||
3. Confirm the deletion.
|
||||
|
||||
## RBAC Administrative Permissions
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
|
||||
# You can invoke CloudFormation and pass the principal ARN from a command line like this:
|
||||
# aws cloudformation create-stack \
|
||||
# --capabilities CAPABILITY_IAM --capabilities CAPABILITY_NAMED_IAM \
|
||||
# --template-body "file://prowler-scan-role.yaml" \
|
||||
# --stack-name "ProwlerScanRole" \
|
||||
# --parameters "ParameterKey=ExternalId,ParameterValue=ProvidedExternalID"
|
||||
|
||||
Description: |
|
||||
This template creates the ProwlerScan IAM Role in this account with
|
||||
all read-only permissions to scan your account for security issues.
|
||||
This template creates the ProwlerScan IAM Role either locally in this account or across
|
||||
multiple accounts via StackSets. It can deploy both simultaneously or just one option.
|
||||
The role includes all read-only permissions to scan your accounts for security issues.
|
||||
Contains two AWS managed policies (SecurityAudit and ViewOnlyAccess) and an inline policy.
|
||||
It sets the trust policy on that IAM Role to permit Prowler to assume that role.
|
||||
This template is designed to be used in Prowler Cloud, but can also be used in other Prowler deployments.
|
||||
|
||||
If you are deploying this template to be used in Prowler Cloud please do not edit the AccountId, IAMPrincipal and ExternalId parameters.
|
||||
|
||||
Parameters:
|
||||
# Core Prowler IAM Role Parameters
|
||||
ExternalId:
|
||||
Description: |
|
||||
This is the External ID that Prowler will use to assume the role ProwlerScan IAM Role.
|
||||
This is the External ID that Prowler will use to assume the ProwlerScan IAM Role.
|
||||
Type: String
|
||||
MinLength: 1
|
||||
AllowedPattern: ".+"
|
||||
ConstraintDescription: "ExternalId must not be empty."
|
||||
|
||||
AccountId:
|
||||
Description: |
|
||||
AWS Account ID that will assume the role created, if you are deploying this template to be used in Prowler Cloud please do not edit this.
|
||||
@@ -31,11 +28,13 @@ Parameters:
|
||||
MaxLength: 12
|
||||
AllowedPattern: "[0-9]{12}"
|
||||
ConstraintDescription: "AccountId must be a valid AWS Account ID."
|
||||
|
||||
IAMPrincipal:
|
||||
Description: |
|
||||
The IAM principal type and name that will be allowed to assume the role created, leave an * for all the IAM principals in your AWS account. If you are deploying this template to be used in Prowler Cloud please do not edit this.
|
||||
Type: String
|
||||
Default: role/prowler*
|
||||
|
||||
EnableOrganizations:
|
||||
Description: |
|
||||
Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account.
|
||||
@@ -45,6 +44,7 @@ Parameters:
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
|
||||
EnableS3Integration:
|
||||
Description: |
|
||||
Enable S3 integration for storing Prowler scan reports.
|
||||
@@ -53,25 +53,102 @@ Parameters:
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
|
||||
S3IntegrationBucketName:
|
||||
Description: |
|
||||
The S3 bucket name where Prowler will store scan reports for your cloud providers.
|
||||
Type: String
|
||||
Default: ""
|
||||
|
||||
S3IntegrationBucketAccountId:
|
||||
Description: |
|
||||
The AWS Account ID owner of the S3 Bucket.
|
||||
Type: String
|
||||
Default: ""
|
||||
|
||||
# Deployment Control Parameters
|
||||
DeployStackSet:
|
||||
Description: |
|
||||
Set to true to deploy the ProwlerScan role across multiple accounts using StackSets.
|
||||
Requires delegated administrator permissions for CloudFormation StackSets.
|
||||
Type: String
|
||||
Default: false
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
|
||||
DeployLocalRole:
|
||||
Description: |
|
||||
Set to true to deploy the ProwlerScan role in this account (the account where this template is deployed).
|
||||
Can be used independently or in conjunction with StackSet deployment.
|
||||
Type: String
|
||||
Default: true
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
|
||||
# StackSet Configuration Parameters
|
||||
AWSOrganizationalUnitId:
|
||||
Description: |
|
||||
AWS Organizations OU to deploy this stackset to (e.g., ou-xxxx-yyyyyyyy or r-xxxx for root).
|
||||
Only required if DeployStackSet is true.
|
||||
Type: String
|
||||
Default: ""
|
||||
AllowedPattern: '^(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})?$'
|
||||
|
||||
RetainStacksOnAccountRemoval:
|
||||
Description: |
|
||||
When an account is removed from the Organization or OU, should the ProwlerScan role remain in that account?
|
||||
False (Recommended for security): Automatically deletes the role when accounts leave, following principle of least privilege.
|
||||
True: Retains the role even after account removal, useful if accounts may temporarily leave and rejoin.
|
||||
Type: String
|
||||
Default: false
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
|
||||
DeployFromDelegatedAdmin:
|
||||
Description: |
|
||||
Is this StackSet being deployed from a Delegated Administrator account (not the Organization Management Account)?
|
||||
True: Deploying from a delegated admin account - uses CallAs: DELEGATED_ADMIN.
|
||||
False: Deploying from the Organization Management Account - omits CallAs property.
|
||||
Only required if DeployStackSet is true.
|
||||
Type: String
|
||||
Default: false
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
|
||||
FailureTolerancePercentage:
|
||||
Description: |
|
||||
The percentage of accounts in which stack operations can fail before CloudFormation stops the operation.
|
||||
Only applies when DeployStackSet is true.
|
||||
Type: Number
|
||||
Default: 10
|
||||
MinValue: 0
|
||||
MaxValue: 100
|
||||
|
||||
Conditions:
|
||||
OrganizationsEnabled: !Equals [!Ref EnableOrganizations, true]
|
||||
S3IntegrationEnabled: !Equals [!Ref EnableS3Integration, true]
|
||||
DeployStackSetEnabled: !Equals [!Ref DeployStackSet, true]
|
||||
DeployLocalRoleEnabled: !Equals [!Ref DeployLocalRole, true]
|
||||
UseDelegatedAdmin: !Equals [!Ref DeployFromDelegatedAdmin, true]
|
||||
|
||||
Rules:
|
||||
S3IntegrationRequiresParams:
|
||||
RuleCondition: !Equals [!Ref EnableS3Integration, "true"]
|
||||
Assertions:
|
||||
- Assert: !Not [!Equals [!Ref S3IntegrationBucketName, ""]]
|
||||
AssertDescription: "S3IntegrationBucketName is required when EnableS3Integration is true."
|
||||
- Assert: !Not [!Equals [!Ref S3IntegrationBucketAccountId, ""]]
|
||||
AssertDescription: "S3IntegrationBucketAccountId is required when EnableS3Integration is true."
|
||||
|
||||
Resources:
|
||||
# Local ProwlerScan Role (deployed in this account)
|
||||
ProwlerScan:
|
||||
Type: AWS::IAM::Role
|
||||
Condition: DeployLocalRoleEnabled
|
||||
Properties:
|
||||
RoleName: ProwlerScan
|
||||
AssumeRolePolicyDocument:
|
||||
@@ -88,8 +165,8 @@ Resources:
|
||||
"aws:PrincipalArn": !Sub "arn:${AWS::Partition}:iam::${AccountId}:${IAMPrincipal}"
|
||||
MaxSessionDuration: 3600
|
||||
ManagedPolicyArns:
|
||||
- "arn:aws:iam::aws:policy/SecurityAudit"
|
||||
- "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
|
||||
- !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit"
|
||||
- !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/ViewOnlyAccess"
|
||||
Policies:
|
||||
- PolicyName: ProwlerScan
|
||||
PolicyDocument:
|
||||
@@ -102,6 +179,7 @@ Resources:
|
||||
- "appstream:Describe*"
|
||||
- "appstream:List*"
|
||||
- "backup:List*"
|
||||
- "backup:Get*"
|
||||
- "bedrock:List*"
|
||||
- "bedrock:Get*"
|
||||
- "cloudtrail:GetInsightSelectors"
|
||||
@@ -127,6 +205,7 @@ Resources:
|
||||
- "glue:GetConnections"
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
- "glue:SearchTables"
|
||||
- "glue:GetMLTransforms"
|
||||
- "lambda:GetFunction*"
|
||||
- "logs:FilterLogEvents"
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
@@ -137,7 +216,6 @@ Resources:
|
||||
- "s3:GetAccountPublicAccessBlock"
|
||||
- "shield:DescribeProtection"
|
||||
- "shield:GetSubscriptionState"
|
||||
- "securityhub:BatchImportFindings"
|
||||
- "securityhub:GetFindings"
|
||||
- "servicecatalog:Describe*"
|
||||
- "servicecatalog:List*"
|
||||
@@ -148,15 +226,20 @@ Resources:
|
||||
- "tag:GetTagKeys"
|
||||
- "wellarchitected:List*"
|
||||
Resource: "*"
|
||||
- Sid: AllowSecurityHubImportFindings
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "securityhub:BatchImportFindings"
|
||||
Resource: "*"
|
||||
- Sid: AllowAPIGatewayReadOnly
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "apigateway:GET"
|
||||
Resource:
|
||||
- "arn:*:apigateway:*::/restapis/*"
|
||||
- "arn:*:apigateway:*::/apis/*"
|
||||
- "arn:*:apigateway:*::/domainnames"
|
||||
- "arn:*:apigateway:*::/domainnames/*"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/restapis/*"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/apis/*"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames/*"
|
||||
- !If
|
||||
- OrganizationsEnabled
|
||||
- PolicyName: ProwlerOrganizations
|
||||
@@ -178,8 +261,12 @@ Resources:
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "organizations:RegisterDelegatedAdministrator"
|
||||
- "iam:CreateServiceLinkedRole"
|
||||
Resource: "*"
|
||||
- Sid: AllowCreateStackSetSLR
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "iam:CreateServiceLinkedRole"
|
||||
Resource: !Sub "arn:${AWS::Partition}:iam::*:role/aws-service-role/member.org.stacksets.cloudformation.amazonaws.com/*"
|
||||
- !Ref AWS::NoValue
|
||||
- !If
|
||||
- S3IntegrationEnabled
|
||||
@@ -222,12 +309,287 @@ Resources:
|
||||
- Key: "Name"
|
||||
Value: "ProwlerScan"
|
||||
|
||||
# StackSet for deploying ProwlerScan role across multiple accounts
|
||||
ProwlerScanStackSet:
|
||||
Type: AWS::CloudFormation::StackSet
|
||||
Condition: DeployStackSetEnabled
|
||||
Properties:
|
||||
StackSetName: !Sub "${AWS::StackName}-ProwlerScan-StackSet"
|
||||
Description: Organizational StackSet to Deploy ProwlerScan IAM Role across accounts
|
||||
PermissionModel: SERVICE_MANAGED
|
||||
CallAs: !If [UseDelegatedAdmin, DELEGATED_ADMIN, !Ref "AWS::NoValue"]
|
||||
Capabilities:
|
||||
- CAPABILITY_NAMED_IAM
|
||||
AutoDeployment:
|
||||
Enabled: True
|
||||
RetainStacksOnAccountRemoval: !Ref RetainStacksOnAccountRemoval
|
||||
OperationPreferences:
|
||||
FailureTolerancePercentage: !Ref FailureTolerancePercentage
|
||||
MaxConcurrentPercentage: 100
|
||||
Parameters:
|
||||
- ParameterKey: ExternalId
|
||||
ParameterValue: !Ref ExternalId
|
||||
- ParameterKey: AccountId
|
||||
ParameterValue: !Ref AccountId
|
||||
- ParameterKey: IAMPrincipal
|
||||
ParameterValue: !Ref IAMPrincipal
|
||||
- ParameterKey: EnableOrganizations
|
||||
ParameterValue: !Ref EnableOrganizations
|
||||
- ParameterKey: EnableS3Integration
|
||||
ParameterValue: !Ref EnableS3Integration
|
||||
- ParameterKey: S3IntegrationBucketName
|
||||
ParameterValue: !Ref S3IntegrationBucketName
|
||||
- ParameterKey: S3IntegrationBucketAccountId
|
||||
ParameterValue: !Ref S3IntegrationBucketAccountId
|
||||
StackInstancesGroup:
|
||||
- DeploymentTargets:
|
||||
OrganizationalUnitIds:
|
||||
- !Ref AWSOrganizationalUnitId
|
||||
Regions:
|
||||
- us-east-1
|
||||
TemplateBody: |
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
|
||||
Description: |
|
||||
This template creates the ProwlerScan IAM Role in this account with
|
||||
all read-only permissions to scan your account for security issues.
|
||||
Contains two AWS managed policies (SecurityAudit and ViewOnlyAccess) and an inline policy.
|
||||
It sets the trust policy on that IAM Role to permit Prowler to assume that role.
|
||||
This template is designed to be used in Prowler Cloud, but can also be used in other Prowler deployments.
|
||||
|
||||
** DEPLOYED VIA SERVICE-MANAGED STACKSET **
|
||||
This stack was automatically deployed across your organization using CloudFormation StackSets
|
||||
with SERVICE_MANAGED permissions. It will auto-deploy to new accounts and can be centrally managed.
|
||||
|
||||
Parameters:
|
||||
ExternalId:
|
||||
Description: |
|
||||
This is the External ID that Prowler will use to assume the role ProwlerScan IAM Role.
|
||||
Type: String
|
||||
MinLength: 1
|
||||
AllowedPattern: ".+"
|
||||
ConstraintDescription: "ExternalId must not be empty."
|
||||
AccountId:
|
||||
Description: |
|
||||
AWS Account ID that will assume the role created, if you are deploying this template to be used in Prowler Cloud please do not edit this.
|
||||
Type: String
|
||||
Default: "232136659152"
|
||||
MinLength: 12
|
||||
MaxLength: 12
|
||||
AllowedPattern: "[0-9]{12}"
|
||||
ConstraintDescription: "AccountId must be a valid AWS Account ID."
|
||||
IAMPrincipal:
|
||||
Description: |
|
||||
The IAM principal type and name that will be allowed to assume the role created, leave an * for all the IAM principals in your AWS account. If you are deploying this template to be used in Prowler Cloud please do not edit this.
|
||||
Type: String
|
||||
Default: role/prowler*
|
||||
EnableOrganizations:
|
||||
Description: |
|
||||
Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account.
|
||||
This adds read-only Organizations permissions (e.g. ListAccounts, DescribeOrganization) and StackSet management permissions.
|
||||
Type: String
|
||||
Default: false
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
EnableS3Integration:
|
||||
Description: |
|
||||
Enable S3 integration for storing Prowler scan reports.
|
||||
Type: String
|
||||
Default: false
|
||||
AllowedValues:
|
||||
- true
|
||||
- false
|
||||
S3IntegrationBucketName:
|
||||
Description: |
|
||||
The S3 bucket name where Prowler will store scan reports for your cloud providers.
|
||||
Type: String
|
||||
Default: ""
|
||||
S3IntegrationBucketAccountId:
|
||||
Description: |
|
||||
The AWS Account ID owner of the S3 Bucket.
|
||||
Type: String
|
||||
Default: ""
|
||||
|
||||
Conditions:
|
||||
OrganizationsEnabled: !Equals [!Ref EnableOrganizations, true]
|
||||
S3IntegrationEnabled: !Equals [!Ref EnableS3Integration, true]
|
||||
|
||||
Resources:
|
||||
ProwlerScan:
|
||||
Type: AWS::IAM::Role
|
||||
Properties:
|
||||
RoleName: ProwlerScan
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
AWS: !Sub "arn:${AWS::Partition}:iam::${AccountId}:root"
|
||||
Action: "sts:AssumeRole"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"sts:ExternalId": !Sub ${ExternalId}
|
||||
StringLike:
|
||||
"aws:PrincipalArn": !Sub "arn:${AWS::Partition}:iam::${AccountId}:${IAMPrincipal}"
|
||||
MaxSessionDuration: 3600
|
||||
ManagedPolicyArns:
|
||||
- !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit"
|
||||
- !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/ViewOnlyAccess"
|
||||
Policies:
|
||||
- PolicyName: ProwlerScan
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: AllowMoreReadOnly
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "account:Get*"
|
||||
- "appstream:Describe*"
|
||||
- "appstream:List*"
|
||||
- "backup:List*"
|
||||
- "backup:Get*"
|
||||
- "bedrock:List*"
|
||||
- "bedrock:Get*"
|
||||
- "cloudtrail:GetInsightSelectors"
|
||||
- "codeartifact:List*"
|
||||
- "codebuild:BatchGet*"
|
||||
- "codebuild:ListReportGroups"
|
||||
- "cognito-idp:GetUserPoolMfaConfig"
|
||||
- "dlm:Get*"
|
||||
- "drs:Describe*"
|
||||
- "ds:Get*"
|
||||
- "ds:Describe*"
|
||||
- "ds:List*"
|
||||
- "dynamodb:GetResourcePolicy"
|
||||
- "ec2:GetEbsEncryptionByDefault"
|
||||
- "ec2:GetSnapshotBlockPublicAccessState"
|
||||
- "ec2:GetInstanceMetadataDefaults"
|
||||
- "ecr:Describe*"
|
||||
- "ecr:GetRegistryScanningConfiguration"
|
||||
- "elasticfilesystem:DescribeBackupPolicy"
|
||||
- "glue:GetConnections"
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
- "glue:SearchTables"
|
||||
- "glue:GetMLTransforms"
|
||||
- "lambda:GetFunction*"
|
||||
- "logs:FilterLogEvents"
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
- "macie2:GetMacieSession"
|
||||
- "macie2:GetAutomatedDiscoveryConfiguration"
|
||||
- "s3:GetAccountPublicAccessBlock"
|
||||
- "shield:DescribeProtection"
|
||||
- "shield:GetSubscriptionState"
|
||||
- "securityhub:GetFindings"
|
||||
- "servicecatalog:Describe*"
|
||||
- "servicecatalog:List*"
|
||||
- "ssm:GetDocument"
|
||||
- "ssm-incidents:List*"
|
||||
- "states:ListTagsForResource"
|
||||
- "support:Describe*"
|
||||
- "tag:GetTagKeys"
|
||||
- "wellarchitected:List*"
|
||||
Resource: "*"
|
||||
- Sid: AllowSecurityHubImportFindings
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "securityhub:BatchImportFindings"
|
||||
Resource: "*"
|
||||
- Sid: AllowAPIGatewayReadOnly
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "apigateway:GET"
|
||||
Resource:
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/restapis/*"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/apis/*"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames"
|
||||
- !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames/*"
|
||||
- !If
|
||||
- OrganizationsEnabled
|
||||
- PolicyName: ProwlerOrganizations
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: AllowOrganizationsReadOnly
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "organizations:DescribeAccount"
|
||||
- "organizations:DescribeOrganization"
|
||||
- "organizations:ListAccounts"
|
||||
- "organizations:ListAccountsForParent"
|
||||
- "organizations:ListOrganizationalUnitsForParent"
|
||||
- "organizations:ListRoots"
|
||||
- "organizations:ListTagsForResource"
|
||||
Resource: "*"
|
||||
- Sid: AllowStackSetManagement
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "organizations:RegisterDelegatedAdministrator"
|
||||
Resource: "*"
|
||||
- Sid: AllowCreateStackSetSLR
|
||||
Effect: Allow
|
||||
Action:
|
||||
- "iam:CreateServiceLinkedRole"
|
||||
Resource: !Sub "arn:${AWS::Partition}:iam::*:role/aws-service-role/member.org.stacksets.cloudformation.amazonaws.com/*"
|
||||
- !Ref AWS::NoValue
|
||||
- !If
|
||||
- S3IntegrationEnabled
|
||||
- PolicyName: S3Integration
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "s3:PutObject"
|
||||
Resource:
|
||||
- !Sub "arn:${AWS::Partition}:s3:::${S3IntegrationBucketName}/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"s3:ResourceAccount": !Sub ${S3IntegrationBucketAccountId}
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "s3:ListBucket"
|
||||
Resource:
|
||||
- !Sub "arn:${AWS::Partition}:s3:::${S3IntegrationBucketName}"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"s3:ResourceAccount": !Sub ${S3IntegrationBucketAccountId}
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- "s3:DeleteObject"
|
||||
Resource:
|
||||
- !Sub "arn:${AWS::Partition}:s3:::${S3IntegrationBucketName}/*test-prowler-connection.txt"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"s3:ResourceAccount": !Sub ${S3IntegrationBucketAccountId}
|
||||
- !Ref AWS::NoValue
|
||||
Tags:
|
||||
- Key: "Service"
|
||||
Value: "https://prowler.com"
|
||||
- Key: "Support"
|
||||
Value: "support@prowler.com"
|
||||
- Key: "CloudFormation"
|
||||
Value: "true"
|
||||
- Key: "Name"
|
||||
Value: "ProwlerScan"
|
||||
|
||||
Outputs:
|
||||
ProwlerScanRoleArn:
|
||||
Description: "ARN of the ProwlerScan IAM Role"
|
||||
Value: !GetAtt ProwlerScan.Arn
|
||||
Export:
|
||||
Name: !Sub "${AWS::StackName}-ProwlerScanRoleArn"
|
||||
|
||||
Metadata:
|
||||
AWS::CloudFormation::StackName: "Prowler"
|
||||
AWS::CloudFormation::Interface:
|
||||
ParameterGroups:
|
||||
- Label:
|
||||
default: Required
|
||||
default: Deployment Options
|
||||
Parameters:
|
||||
- DeployLocalRole
|
||||
- DeployStackSet
|
||||
- Label:
|
||||
default: Required Prowler Configuration
|
||||
Parameters:
|
||||
- ExternalId
|
||||
- AccountId
|
||||
@@ -235,14 +597,27 @@ Metadata:
|
||||
- EnableOrganizations
|
||||
- EnableS3Integration
|
||||
- Label:
|
||||
default: Optional
|
||||
default: Optional S3 Integration
|
||||
Parameters:
|
||||
- S3IntegrationBucketName
|
||||
- S3IntegrationBucketAccountId
|
||||
- Label:
|
||||
default: StackSet Configuration (Required if DeployStackSet is true)
|
||||
Parameters:
|
||||
- AWSOrganizationalUnitId
|
||||
- DeployFromDelegatedAdmin
|
||||
- RetainStacksOnAccountRemoval
|
||||
- FailureTolerancePercentage
|
||||
|
||||
Outputs:
|
||||
ProwlerScanRoleArn:
|
||||
Description: "ARN of the ProwlerScan IAM Role"
|
||||
LocalProwlerScanRoleArn:
|
||||
Condition: DeployLocalRoleEnabled
|
||||
Description: "ARN of the ProwlerScan IAM Role deployed locally in this account"
|
||||
Value: !GetAtt ProwlerScan.Arn
|
||||
Export:
|
||||
Name: !Sub "${AWS::StackName}-ProwlerScanRoleArn"
|
||||
|
||||
StackSetId:
|
||||
Condition: DeployStackSetEnabled
|
||||
Description: "StackSet ID for the ProwlerScan role deployment across accounts"
|
||||
Value: !Ref ProwlerScanStackSet
|
||||
|
||||
@@ -4,7 +4,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
|
||||
## [5.33.1] (Prowler v5.33.1)
|
||||
|
||||
### 🐞 Fixed
|
||||
@@ -17,6 +16,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Azure Function App optional permission failures now log as warnings, and Function App environment variable fields use the correct spelling internally [(#11926)](https://github.com/prowler-cloud/prowler/pull/11926)
|
||||
|
||||
---
|
||||
|
||||
## [5.33.0] (Prowler v5.33.0)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
`ec2_instance_account_imdsv2_enabled` findings now use regional resource ARNs, preventing findings from different AWS Regions from collapsing into one resource
|
||||
@@ -0,0 +1 @@
|
||||
EC2 AMI loading now targets Amazon-owned AMIs used by audited instances, reducing AWS API calls during EC2 scans
|
||||
@@ -899,6 +899,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-6",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
@@ -1238,9 +1239,12 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-west-2"
|
||||
],
|
||||
@@ -1310,6 +1314,7 @@
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-west-2",
|
||||
"sa-east-1",
|
||||
"us-east-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
@@ -1584,9 +1589,13 @@
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -2681,6 +2690,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-6",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
@@ -3185,6 +3195,7 @@
|
||||
"connecthealth": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"eu-west-2",
|
||||
"us-east-1",
|
||||
"us-west-2"
|
||||
],
|
||||
@@ -3845,7 +3856,9 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-6",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -4619,7 +4632,12 @@
|
||||
},
|
||||
"elementalinference": {
|
||||
"regions": {
|
||||
"aws": [],
|
||||
"aws": [
|
||||
"ap-south-1",
|
||||
"eu-west-1",
|
||||
"us-east-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": []
|
||||
@@ -6899,6 +6917,7 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -9260,9 +9279,7 @@
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [
|
||||
"cn-north-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
@@ -10281,6 +10298,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-6",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
@@ -10797,7 +10815,10 @@
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": []
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"sagemaker": {
|
||||
@@ -13436,6 +13457,7 @@
|
||||
"wisdom": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-southeast-1",
|
||||
|
||||
@@ -18,4 +18,4 @@ class ec2_ami_public(Check):
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
return findings
|
||||
|
||||
@@ -14,7 +14,11 @@ class ec2_instance_account_imdsv2_enabled(Check):
|
||||
metadata=self.metadata(),
|
||||
resource=instance_metadata_default,
|
||||
)
|
||||
report.resource_arn = ec2_client.account_arn_template
|
||||
report.resource_arn = (
|
||||
f"arn:{ec2_client.audited_partition}:ec2:"
|
||||
f"{instance_metadata_default.region}:"
|
||||
f"{ec2_client.audited_account}:account"
|
||||
)
|
||||
report.resource_id = ec2_client.audited_account
|
||||
if instance_metadata_default.http_tokens == "required":
|
||||
report.status = "PASS"
|
||||
|
||||
@@ -26,11 +26,12 @@ class ec2_instance_with_outdated_ami(Check):
|
||||
List[Check_Report_AWS]: A list containing the results of the check for each instance.
|
||||
"""
|
||||
findings = []
|
||||
images_by_id = getattr(ec2_client, "images_by_id", None)
|
||||
if images_by_id is None:
|
||||
images_by_id = {image.id: image for image in ec2_client.images}
|
||||
|
||||
for instance in ec2_client.instances:
|
||||
ami = next(
|
||||
(image for image in ec2_client.images if image.id == instance.image_id),
|
||||
None,
|
||||
)
|
||||
ami = images_by_id.get(instance.image_id)
|
||||
if ami and ami.owner == "amazon":
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=instance)
|
||||
report.status = "PASS"
|
||||
|
||||
@@ -13,6 +13,8 @@ from prowler.lib.resource_limit import (
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
DESCRIBE_IMAGES_IMAGE_IDS_BATCH_SIZE = 200
|
||||
|
||||
|
||||
class EC2(AWSService):
|
||||
def __init__(self, provider):
|
||||
@@ -39,6 +41,7 @@ class EC2(AWSService):
|
||||
self.network_interfaces = {}
|
||||
self.__threading_call__(self._describe_network_interfaces)
|
||||
self.images = []
|
||||
self.images_by_id = {}
|
||||
self.__threading_call__(self._describe_images)
|
||||
self.volumes = []
|
||||
self.__threading_call__(self._describe_volumes)
|
||||
@@ -374,36 +377,90 @@ class EC2(AWSService):
|
||||
|
||||
def _describe_images(self, regional_client):
|
||||
try:
|
||||
for owner in ["self", "amazon"]:
|
||||
try:
|
||||
for image in regional_client.describe_images(
|
||||
Owners=[owner], IncludeDeprecated=True
|
||||
)["Images"]:
|
||||
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:image/{image['ImageId']}"
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
self.images.append(
|
||||
Image(
|
||||
id=image["ImageId"],
|
||||
arn=arn,
|
||||
name=image.get("Name", ""),
|
||||
public=image.get("Public", False),
|
||||
region=regional_client.region,
|
||||
tags=image.get("Tags"),
|
||||
deprecation_time=image.get("DeprecationTime"),
|
||||
owner=owner,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
try:
|
||||
for image in regional_client.describe_images(
|
||||
Owners=["self"], IncludeDeprecated=True
|
||||
)["Images"]:
|
||||
self._add_image(image, regional_client.region, "self")
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
amazon_image_ids = sorted(
|
||||
{
|
||||
instance.image_id
|
||||
for instance in self.instances
|
||||
if instance.region == regional_client.region
|
||||
and instance.image_id
|
||||
and instance.image_id not in self.images_by_id
|
||||
}
|
||||
)
|
||||
|
||||
for image_batch in self._get_image_id_batches(amazon_image_ids):
|
||||
for image in self._describe_images_by_id(regional_client, image_batch):
|
||||
if self._is_amazon_image(image):
|
||||
self._add_image(image, regional_client.region, "amazon")
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _add_image(self, image, region, owner):
|
||||
arn = f"arn:{self.audited_partition}:ec2:{region}:{self.audited_account}:image/{image['ImageId']}"
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
ec2_image = Image(
|
||||
id=image["ImageId"],
|
||||
arn=arn,
|
||||
name=image.get("Name", ""),
|
||||
public=image.get("Public", False),
|
||||
region=region,
|
||||
tags=image.get("Tags"),
|
||||
deprecation_time=image.get("DeprecationTime"),
|
||||
owner=owner,
|
||||
)
|
||||
self.images.append(ec2_image)
|
||||
self.images_by_id[ec2_image.id] = ec2_image
|
||||
|
||||
def _describe_images_by_id(self, regional_client, image_ids):
|
||||
try:
|
||||
return regional_client.describe_images(
|
||||
ImageIds=image_ids, IncludeDeprecated=True
|
||||
)["Images"]
|
||||
except ClientError as error:
|
||||
if error.response["Error"]["Code"] == "InvalidAMIID.NotFound":
|
||||
if len(image_ids) == 1:
|
||||
logger.warning(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return []
|
||||
|
||||
midpoint = len(image_ids) // 2
|
||||
return self._describe_images_by_id(
|
||||
regional_client, image_ids[:midpoint]
|
||||
) + self._describe_images_by_id(regional_client, image_ids[midpoint:])
|
||||
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return []
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _get_image_id_batches(image_ids):
|
||||
for index in range(0, len(image_ids), DESCRIBE_IMAGES_IMAGE_IDS_BATCH_SIZE):
|
||||
yield image_ids[index : index + DESCRIBE_IMAGES_IMAGE_IDS_BATCH_SIZE]
|
||||
|
||||
@staticmethod
|
||||
def _is_amazon_image(image):
|
||||
return image.get("ImageOwnerAlias") == "amazon"
|
||||
|
||||
def _describe_volumes(self, regional_client):
|
||||
try:
|
||||
describe_volumes_paginator = regional_client.get_paginator(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when __init__.py files are present inside test directories."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from os import walk
|
||||
from pathlib import Path
|
||||
|
||||
EXCLUDED_TEST_INIT_ROOTS = {
|
||||
Path("tests/lib/check/fixtures/checks_folder"),
|
||||
}
|
||||
|
||||
IGNORED_DIRECTORY_NAMES = {
|
||||
".git",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pytest_cache",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pycache__",
|
||||
"node_modules",
|
||||
"venv",
|
||||
}
|
||||
|
||||
|
||||
def is_test_init_file(path: Path) -> bool:
|
||||
"""Return True when the file is a root tests __init__.py."""
|
||||
return path.name == "__init__.py" and path.parts[0] == "tests"
|
||||
|
||||
|
||||
def is_excluded_test_init_file(path: Path, root: Path) -> bool:
|
||||
"""Return True when the file belongs to an allowed fixture directory."""
|
||||
relative_path = path.relative_to(root)
|
||||
return any(
|
||||
relative_path.is_relative_to(excluded) for excluded in EXCLUDED_TEST_INIT_ROOTS
|
||||
)
|
||||
|
||||
|
||||
def find_test_init_files(root: Path) -> list[Path]:
|
||||
"""Return sorted __init__.py files found under test directories."""
|
||||
matches = []
|
||||
|
||||
for current_root, directories, filenames in walk(root):
|
||||
directories[:] = [
|
||||
directory
|
||||
for directory in directories
|
||||
if directory not in IGNORED_DIRECTORY_NAMES
|
||||
]
|
||||
if "__init__.py" in filenames:
|
||||
path = Path(current_root) / "__init__.py"
|
||||
else:
|
||||
continue
|
||||
|
||||
relative_path = path.relative_to(root)
|
||||
if is_test_init_file(relative_path) and not is_excluded_test_init_file(
|
||||
path, root
|
||||
):
|
||||
matches.append(path)
|
||||
|
||||
return sorted(matches)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"root",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Repository root to scan. Defaults to the current directory.",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
matches = find_test_init_files(root)
|
||||
|
||||
if not matches:
|
||||
print("No __init__.py files found in test directories.")
|
||||
return 0
|
||||
|
||||
print("Remove __init__.py files from test directories:")
|
||||
for path in matches:
|
||||
print(path.relative_to(root))
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -153,9 +153,9 @@ The `pr-check-changelog.yml` workflow enforces fragments:
|
||||
|
||||
## Release flow (compile)
|
||||
|
||||
- At release time, the `compile-changelogs` workflow (manual dispatch: `prowler_version` + `target_branch`; per-component versions are auto-derived from each changelog's latest stamped heading plus the pending fragment types, with optional explicit overrides or `skip`) resolves each fragment's PR from git history, runs the compiler per component, and opens a `chore(changelog): vX.Y.Z` PR (labeled `no-changelog`) that inserts the stamped `## [X.Y.Z] (Prowler vX.Y.Z)` block into each `CHANGELOG.md` and deletes the consumed fragments. A human reviews and squash-merges it. `prepare-release.yml` then extracts the stamped sections exactly as before.
|
||||
- At release time, the `compile-changelogs` workflow (manual dispatch: `prowler_version` + `target_branch`; per-component versions are auto-derived by mirroring the Prowler version — SDK mirrors it directly, UI is `1.<minor>.<patch>`, API is `1.<minor + 1>.<patch>`, and only the MCP Server derives from its pending fragment types — with optional explicit overrides or `skip`) resolves each fragment's PR from git history, runs the compiler per component, and opens a `chore(changelog): vX.Y.Z` PR (labeled `no-changelog` and `skip-sync`) that inserts the stamped `## [X.Y.Z] (Prowler vX.Y.Z)` block into each `CHANGELOG.md` and deletes the consumed fragments. A human reviews and squash-merges it. `prepare-release.yml` then extracts the stamped sections exactly as before.
|
||||
- **Minor release (X.Y.0):** compile on `master` and merge the compile PR BEFORE cutting the `v5.X` branch.
|
||||
- **Patch release (X.Y.Z):** fixes are backported to `v5.X` with their fragment files (conflict-free); compile on `v5.X` and merge its PR there. The same workflow run automatically opens a second forward-sync PR against master (labeled `no-changelog`) that inserts the same stamped block under master's marker and deletes the consumed fragments, so the next minor cannot re-release them; merge it right after. Fragments that only existed on `v5.X` are skipped with a notice. No manual git is involved.
|
||||
- **Patch release (X.Y.Z):** fixes are backported to `v5.X` with their fragment files (conflict-free); compile on `v5.X` and merge its PR there. The same workflow run automatically opens a second forward-sync PR against master (labeled `no-changelog` and `skip-sync`) that inserts the same stamped block under master's marker and deletes the consumed fragments, so the next minor cannot re-release them; merge it right after. Fragments that only existed on `v5.X` are skipped with a notice. No manual git is involved.
|
||||
- Entries within a section are ordered by PR number ascending (approximately chronological). Do not fight this ordering.
|
||||
|
||||
## Fixing an already-released entry
|
||||
|
||||
@@ -312,3 +312,68 @@ def test_compile_workflow_requires_removed_fragments_in_major_releases():
|
||||
assert "effective_major" in workflow
|
||||
assert "effective_minor" in workflow
|
||||
assert "effective_patch" in workflow
|
||||
|
||||
|
||||
def test_compile_workflow_prs_skip_cloud_sync():
|
||||
workflow = read_workflow("compile-changelogs.yml")
|
||||
labels_blocks = re.findall(r"labels: \|\n((?:\s+[a-z-]+\n)+)", workflow)
|
||||
|
||||
assert len(labels_blocks) == 2
|
||||
for block in labels_blocks:
|
||||
assert "no-changelog" in block.split()
|
||||
assert "skip-sync" in block.split()
|
||||
|
||||
|
||||
def test_compile_workflow_auto_derives_versions_by_mirroring_prowler_version():
|
||||
workflow = read_workflow("compile-changelogs.yml")
|
||||
|
||||
assert 'prowler) effective="$PROWLER_VERSION" ;;' in workflow
|
||||
assert 'ui) effective="1.${prowler_minor}.${prowler_patch}" ;;' in workflow
|
||||
assert 'api) effective="1.$((prowler_minor + 1)).${prowler_patch}" ;;' in workflow
|
||||
assert (
|
||||
"auto-derived version '${effective}' is not greater than the latest released version"
|
||||
in workflow
|
||||
)
|
||||
|
||||
|
||||
def test_compile_workflow_auto_derives_mcp_patch_bumps_on_patch_releases():
|
||||
workflow = read_workflow("compile-changelogs.yml")
|
||||
|
||||
assert "'added'/'deprecated' fragments are shipping in a Prowler patch" in workflow
|
||||
assert (
|
||||
"elif echo \"$fragments\" | grep -qE '\\.(added|changed|deprecated)(\\.[0-9]+)?\\.md$'; then"
|
||||
in workflow
|
||||
)
|
||||
|
||||
|
||||
def test_forward_sync_pads_release_blocks_with_blank_lines():
|
||||
workflow = read_workflow("compile-changelogs.yml")
|
||||
|
||||
assert "block-normalized.md" in workflow
|
||||
assert 'total_lines=$(wc -l < "$changelog")' in workflow
|
||||
assert '[ -n "$(sed -n "$((insertion_line - 1))p" "$changelog")" ]' in workflow
|
||||
assert 'if [ "$insertion_line" -le "$total_lines" ]; then' in workflow
|
||||
|
||||
|
||||
def test_component_changelogs_separate_release_blocks_with_blank_lines():
|
||||
for component in COMPONENTS:
|
||||
lines = (REPO_ROOT / component / "CHANGELOG.md").read_text().splitlines()
|
||||
marker_line = lines.index("<!-- changelog: release notes start -->")
|
||||
|
||||
assert (
|
||||
lines[marker_line + 1] == ""
|
||||
), f"{component}/CHANGELOG.md: expected a blank line after the marker"
|
||||
assert (
|
||||
lines[marker_line + 2] != ""
|
||||
), f"{component}/CHANGELOG.md: expected a single blank line after the marker"
|
||||
for index, line in enumerate(lines):
|
||||
if line == "---" and index + 1 < len(lines):
|
||||
assert lines[index + 1] == "", (
|
||||
f"{component}/CHANGELOG.md line {index + 2}: "
|
||||
"expected a blank line after '---'"
|
||||
)
|
||||
if line.startswith("## ["):
|
||||
assert lines[index - 1] == "", (
|
||||
f"{component}/CHANGELOG.md line {index}: "
|
||||
"expected a blank line before a release heading"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_PATH = (
|
||||
Path(__file__).resolve().parents[2] / "scripts" / "check_test_init_files.py"
|
||||
)
|
||||
|
||||
|
||||
def load_guard_module():
|
||||
spec = spec_from_file_location("check_test_init_files", SCRIPT_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_find_test_init_files_detects_only_root_test_directories(tmp_path):
|
||||
guard = load_guard_module()
|
||||
|
||||
(tmp_path / "tests" / "providers" / "aws").mkdir(parents=True)
|
||||
(tmp_path / "tests" / "providers" / "aws" / "__init__.py").write_text("")
|
||||
(tmp_path / "api" / "tests" / "performance").mkdir(parents=True)
|
||||
(tmp_path / "api" / "tests" / "performance" / "__init__.py").write_text("")
|
||||
(tmp_path / "mcp_server" / "tests").mkdir(parents=True)
|
||||
(tmp_path / "mcp_server" / "tests" / "__init__.py").write_text("")
|
||||
(tmp_path / "prowler" / "providers" / "aws").mkdir(parents=True)
|
||||
(tmp_path / "prowler" / "providers" / "aws" / "__init__.py").write_text("")
|
||||
(
|
||||
tmp_path / "tests" / "lib" / "check" / "fixtures" / "checks_folder" / "check11"
|
||||
).mkdir(parents=True)
|
||||
(
|
||||
tmp_path
|
||||
/ "tests"
|
||||
/ "lib"
|
||||
/ "check"
|
||||
/ "fixtures"
|
||||
/ "checks_folder"
|
||||
/ "check11"
|
||||
/ "__init__.py"
|
||||
).write_text("")
|
||||
|
||||
matches = guard.find_test_init_files(tmp_path)
|
||||
|
||||
assert [path.relative_to(tmp_path) for path in matches] == [
|
||||
Path("tests/providers/aws/__init__.py"),
|
||||
]
|
||||
|
||||
|
||||
def test_find_test_init_files_ignores_virtualenv_test_packages(tmp_path):
|
||||
guard = load_guard_module()
|
||||
|
||||
virtualenv_tests = (
|
||||
tmp_path
|
||||
/ ".venv"
|
||||
/ "lib"
|
||||
/ "python3.12"
|
||||
/ "site-packages"
|
||||
/ "package"
|
||||
/ "tests"
|
||||
)
|
||||
virtualenv_tests.mkdir(parents=True)
|
||||
(virtualenv_tests / "__init__.py").write_text("")
|
||||
(tmp_path / "tests" / "providers" / "aws").mkdir(parents=True)
|
||||
(tmp_path / "tests" / "providers" / "aws" / "__init__.py").write_text("")
|
||||
|
||||
matches = guard.find_test_init_files(tmp_path)
|
||||
|
||||
assert [path.relative_to(tmp_path) for path in matches] == [
|
||||
Path("tests/providers/aws/__init__.py"),
|
||||
]
|
||||
|
||||
|
||||
def test_main_returns_error_when_test_init_files_exist(tmp_path, capsys):
|
||||
guard = load_guard_module()
|
||||
|
||||
(tmp_path / "tests" / "config").mkdir(parents=True)
|
||||
(tmp_path / "tests" / "config" / "__init__.py").write_text("")
|
||||
|
||||
assert guard.main([str(tmp_path)]) == 1
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Remove __init__.py files from test directories" in captured.out
|
||||
assert "tests/config/__init__.py" in captured.out
|
||||
|
||||
|
||||
def test_repository_has_no_test_init_files():
|
||||
guard = load_guard_module()
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
|
||||
assert guard.find_test_init_files(repo_root) == []
|
||||