Merge remote-tracking branch 'origin/master' into feature/eslint-typescript-flat

This commit is contained in:
Pablo F.G
2026-07-14 08:31:46 +02:00
245 changed files with 1723 additions and 203 deletions
+4
View File
@@ -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 .
+64 -15
View File
@@ -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
+1 -1
View File
@@ -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 }}
+2
View File
@@ -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}
+1 -1
View File
@@ -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
+1
View File
@@ -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,
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

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
+36 -33
View File
@@ -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
+1 -1
View File
@@ -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(
+90
View File
@@ -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())
+2 -2
View File
@@ -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
View File
View File
View File
View File
View File
+65
View File
@@ -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"
)
+93
View File
@@ -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) == []
View File
View File

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