diff --git a/.github/renovate.json b/.github/renovate.json index 14e1726b71..1cc3d7570b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -23,6 +23,10 @@ "prConcurrentLimit": 20, "prHourlyLimit": 10, "vulnerabilityAlerts": { + "labels": [ + "dependencies", + "security" + ], "prHourlyLimit": 0, "prConcurrentLimit": 0 }, diff --git a/.github/workflows/check-test-init-files.yml b/.github/workflows/check-test-init-files.yml new file mode 100644 index 0000000000..ef66b56e42 --- /dev/null +++ b/.github/workflows/check-test-init-files.yml @@ -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 . diff --git a/.github/workflows/compile-changelogs.yml b/.github/workflows/compile-changelogs.yml index 2634f65a91..308bba9553 100644 --- a/.github/workflows/compile-changelogs.yml +++ b/.github/workflows/compile-changelogs.yml @@ -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..; "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..; "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.., and the + # API is the independent 1.. 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" | 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 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2281cb3b09..01f5abcebc 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -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 }} diff --git a/Dockerfile b/Dockerfile index 88183f652c..25808ab8e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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} diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index ff78a2d8e4..9e8b541130 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -4,7 +4,6 @@ All notable changes to the **Prowler API** are documented in this file. - ## [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 diff --git a/docs/docs.json b/docs/docs.json index 8c5f2d87fb..4284f53a2a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -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, diff --git a/docs/images/compliance/prowler-app-cross-provider-detail.png b/docs/images/compliance/prowler-app-cross-provider-detail.png new file mode 100644 index 0000000000..8ee82562e2 Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-detail.png differ diff --git a/docs/images/compliance/prowler-app-cross-provider-overview.png b/docs/images/compliance/prowler-app-cross-provider-overview.png new file mode 100644 index 0000000000..d818da1b80 Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-overview.png differ diff --git a/docs/images/compliance/prowler-app-cross-provider-report.png b/docs/images/compliance/prowler-app-cross-provider-report.png new file mode 100644 index 0000000000..e3b45ef74e Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-report.png differ diff --git a/docs/images/compliance/prowler-app-cross-provider-requirements-accordion.png b/docs/images/compliance/prowler-app-cross-provider-requirements-accordion.png new file mode 100644 index 0000000000..cd87e3ee35 Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-requirements-accordion.png differ diff --git a/docs/images/compliance/prowler-app-cross-provider-tab.png b/docs/images/compliance/prowler-app-cross-provider-tab.png new file mode 100644 index 0000000000..b3aa8ed2b5 Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-tab.png differ diff --git a/docs/images/prowler-app/rbac/provider_group.png b/docs/images/prowler-app/rbac/provider_group.png index 878306e02f..8698cc9c41 100644 Binary files a/docs/images/prowler-app/rbac/provider_group.png and b/docs/images/prowler-app/rbac/provider_group.png differ diff --git a/docs/images/prowler-app/rbac/provider_group_edit.png b/docs/images/prowler-app/rbac/provider_group_edit.png index 5de9649578..3d37b329b7 100644 Binary files a/docs/images/prowler-app/rbac/provider_group_edit.png and b/docs/images/prowler-app/rbac/provider_group_edit.png differ diff --git a/docs/images/prowler-app/rbac/provider_group_edit_1.png b/docs/images/prowler-app/rbac/provider_group_edit_1.png index ed4a090eed..4369983a20 100644 Binary files a/docs/images/prowler-app/rbac/provider_group_edit_1.png and b/docs/images/prowler-app/rbac/provider_group_edit_1.png differ diff --git a/docs/images/prowler-app/rbac/provider_group_remove.png b/docs/images/prowler-app/rbac/provider_group_remove.png index 580a8533cf..f504617159 100644 Binary files a/docs/images/prowler-app/rbac/provider_group_remove.png and b/docs/images/prowler-app/rbac/provider_group_remove.png differ diff --git a/docs/images/prowler-app/rbac/role_create_1.png b/docs/images/prowler-app/rbac/role_create_1.png index a96b0ac9ef..b88fe990db 100644 Binary files a/docs/images/prowler-app/rbac/role_create_1.png and b/docs/images/prowler-app/rbac/role_create_1.png differ diff --git a/docs/user-guide/compliance/tutorials/compliance.mdx b/docs/user-guide/compliance/tutorials/compliance.mdx index 63b2502b25..e1ec152afb 100644 --- a/docs/user-guide/compliance/tutorials/compliance.mdx +++ b/docs/user-guide/compliance/tutorials/compliance.mdx @@ -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) diff --git a/docs/user-guide/compliance/tutorials/cross-provider-compliance.mdx b/docs/user-guide/compliance/tutorials/cross-provider-compliance.mdx new file mode 100644 index 0000000000..617a3680ed --- /dev/null +++ b/docs/user-guide/compliance/tutorials/cross-provider-compliance.mdx @@ -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" + + + +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. + + + +## 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/.json`), as opposed to the legacy per-provider frameworks under `prowler/compliance//`. 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. + + +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. + + +### 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 + + + + Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) and select **Compliance** from the left navigation. + + + 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. + + + +Compliance page showing the Per Scan and Cross-provider tabs, with the Cross-provider tab highlighted + + +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. + + +## Exploring the Overview + +The overview presents one card per supported universal framework, each summarizing the consolidated posture across every contributing provider. + +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 + +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. + + +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)). + + +## 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. + +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 + +### 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. + +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 + +## 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**. + + +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. + + +### 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. + + +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. + + +### Generating and Reusing a Report + +Because the report aggregates many scans, it is generated **asynchronously**: + + + + Select **Report β†’ Generate new report…**, optionally give it a name, and confirm. Prowler starts a background job and shows a "Report generation started" confirmation. + + + 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. + + + 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. + + + +Report dropdown on the Cross-Provider Compliance detail page showing the Generate new report option + +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. + + +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. + + +## 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) diff --git a/docs/user-guide/compliance/tutorials/threatscore.mdx b/docs/user-guide/compliance/tutorials/threatscore.mdx index bb5a006342..492f84c5d2 100644 --- a/docs/user-guide/compliance/tutorials/threatscore.mdx +++ b/docs/user-guide/compliance/tutorials/threatscore.mdx @@ -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 diff --git a/docs/user-guide/tutorials/prowler-app-rbac.mdx b/docs/user-guide/tutorials/prowler-app-rbac.mdx index fa4ecfa196..0cd22f20e7 100644 --- a/docs/user-guide/tutorials/prowler-app-rbac.mdx +++ b/docs/user-guide/tutorials/prowler-app-rbac.mdx @@ -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. **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 ### 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. - Create Provider Group +4. Select the providers that the group controls. Optionally, select the roles that should use the group. + +5. Click **Create Group**. + + Create a Provider Group #### 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**. - Edit Provider Group + Edit Provider Group action -3. Change the provider group parameters you need and save the changes. +3. Update the group name, providers, or roles, and save the changes. - Edit Provider Group Details + Edit Provider Group form #### 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**. - Remove Provider Group +3. Confirm the deletion. + + Delete Provider Group confirmation ### 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**. - Create Role +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. Role parameters +5. Click **Add Role**. -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. #### 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**. - Edit Role - -3. Adjust the settings as needed and save the changes. - - Edit Role Details +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. - - Remove Role +2. Open the actions menu for the role and click **Delete Role**. +3. Confirm the deletion. ## RBAC Administrative Permissions diff --git a/permissions/templates/cloudformation/prowler-scan-role.yml b/permissions/templates/cloudformation/prowler-scan-role.yml index 744c8d3e23..96e6f7b2a5 100644 --- a/permissions/templates/cloudformation/prowler-scan-role.yml +++ b/permissions/templates/cloudformation/prowler-scan-role.yml @@ -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 diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 15b0142163..3a9ebff986 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -4,7 +4,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - ## [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 diff --git a/prowler/changelog.d/ec2-imdsv2-regional-resource-identity.fixed.md b/prowler/changelog.d/ec2-imdsv2-regional-resource-identity.fixed.md new file mode 100644 index 0000000000..97e854ce61 --- /dev/null +++ b/prowler/changelog.d/ec2-imdsv2-regional-resource-identity.fixed.md @@ -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 diff --git a/prowler/changelog.d/ec2-targeted-ami-loading.fixed.md b/prowler/changelog.d/ec2-targeted-ami-loading.fixed.md new file mode 100644 index 0000000000..6fed547536 --- /dev/null +++ b/prowler/changelog.d/ec2-targeted-ami-loading.fixed.md @@ -0,0 +1 @@ +EC2 AMI loading now targets Amazon-owned AMIs used by audited instances, reducing AWS API calls during EC2 scans diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 1fa83ab593..6ee2b8d627 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -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", diff --git a/prowler/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public.py b/prowler/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public.py index 3df5fffb58..a708467a94 100644 --- a/prowler/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public.py +++ b/prowler/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public.py @@ -18,4 +18,4 @@ class ec2_ami_public(Check): findings.append(report) - return findings + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.py b/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.py index 9abd6239cc..f6b0fce95f 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.py +++ b/prowler/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled.py @@ -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" diff --git a/prowler/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami.py b/prowler/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami.py index b0dc677e34..3e4d17f4fd 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami.py +++ b/prowler/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami.py @@ -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" diff --git a/prowler/providers/aws/services/ec2/ec2_service.py b/prowler/providers/aws/services/ec2/ec2_service.py index e630d9a1c7..1c1055ba78 100644 --- a/prowler/providers/aws/services/ec2/ec2_service.py +++ b/prowler/providers/aws/services/ec2/ec2_service.py @@ -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( diff --git a/scripts/check_test_init_files.py b/scripts/check_test_init_files.py new file mode 100644 index 0000000000..ddb94548e6 --- /dev/null +++ b/scripts/check_test_init_files.py @@ -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()) diff --git a/skills/prowler-changelog/SKILL.md b/skills/prowler-changelog/SKILL.md index 369da2ba55..fef70fa9bc 100644 --- a/skills/prowler-changelog/SKILL.md +++ b/skills/prowler-changelog/SKILL.md @@ -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..`, API is `1..`, 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 diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/config/__init__.py b/tests/config/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/config/schema/__init__.py b/tests/config/schema/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dashboard/__init__.py b/tests/dashboard/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dashboard/compliance/__init__.py b/tests/dashboard/compliance/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dashboard/pages/__init__.py b/tests/dashboard/pages/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/github/changelog_fragments_test.py b/tests/github/changelog_fragments_test.py index 13cf65b389..05a751125e 100644 --- a/tests/github/changelog_fragments_test.py +++ b/tests/github/changelog_fragments_test.py @@ -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("") + + 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" + ) diff --git a/tests/lib/check_test_init_files_test.py b/tests/lib/check_test_init_files_test.py new file mode 100644 index 0000000000..2dc4d77902 --- /dev/null +++ b/tests/lib/check_test_init_files_test.py @@ -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) == [] diff --git a/tests/lib/outputs/compliance/asd_essential_eight/__init__.py b/tests/lib/outputs/compliance/asd_essential_eight/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/c5/__init__.py b/tests/lib/outputs/compliance/c5/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/ccc/__init__.py b/tests/lib/outputs/compliance/ccc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/kisa_ismsp/__init__.py b/tests/lib/outputs/compliance/kisa_ismsp/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/mitre_attack/__init__.py b/tests/lib/outputs/compliance/mitre_attack/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/okta_idaas_stig/__init__.py b/tests/lib/outputs/compliance/okta_idaas_stig/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/prowler_threatscore/__init__.py b/tests/lib/outputs/compliance/prowler_threatscore/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/compliance/universal/__init__.py b/tests/lib/outputs/compliance/universal/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/outputs/jira/__init__.py b/tests/lib/outputs/jira/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/timeline/__init__.py b/tests/lib/timeline/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/lib/timeline/models_test.py b/tests/lib/timeline/timeline_models_test.py similarity index 100% rename from tests/lib/timeline/models_test.py rename to tests/lib/timeline/timeline_models_test.py diff --git a/tests/providers/alibabacloud/__init__.py b/tests/providers/alibabacloud/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/lib/__init__.py b/tests/providers/alibabacloud/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/lib/mutelist/__init__.py b/tests/providers/alibabacloud/lib/mutelist/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/__init__.py b/tests/providers/alibabacloud/services/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/actiontrail/__init__.py b/tests/providers/alibabacloud/services/actiontrail/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/actiontrail/actiontrail_multi_region_enabled/__init__.py b/tests/providers/alibabacloud/services/actiontrail/actiontrail_multi_region_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/actiontrail/actiontrail_oss_bucket_not_publicly_accessible/__init__.py b/tests/providers/alibabacloud/services/actiontrail/actiontrail_oss_bucket_not_publicly_accessible/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/__init__.py b/tests/providers/alibabacloud/services/cs/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_cloudmonitor_enabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_cloudmonitor_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_recent/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_recent/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_weekly/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_weekly/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_dashboard_disabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_dashboard_disabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_eni_multiple_ip_enabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_eni_multiple_ip_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_log_service_enabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_log_service_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_network_policy_enabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_network_policy_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_private_cluster_enabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_private_cluster_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/cs/cs_kubernetes_rbac_enabled/__init__.py b/tests/providers/alibabacloud/services/cs/cs_kubernetes_rbac_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/__init__.py b/tests/providers/alibabacloud/services/ecs/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_attached_disk_encrypted/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_attached_disk_encrypted/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_instance_endpoint_protection_installed/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_instance_endpoint_protection_installed/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_instance_latest_os_patches_applied/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_instance_latest_os_patches_applied/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_instance_no_legacy_network/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_instance_no_legacy_network/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_rdp_internet/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_rdp_internet/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_ssh_internet/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_ssh_internet/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ecs/ecs_unattached_disk_encrypted/__init__.py b/tests/providers/alibabacloud/services/ecs/ecs_unattached_disk_encrypted/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/oss/__init__.py b/tests/providers/alibabacloud/services/oss/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/oss/oss_bucket_logging_enabled/__init__.py b/tests/providers/alibabacloud/services/oss/oss_bucket_logging_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/oss/oss_bucket_not_publicly_accessible/__init__.py b/tests/providers/alibabacloud/services/oss/oss_bucket_not_publicly_accessible/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/oss/oss_bucket_secure_transport_enabled/__init__.py b/tests/providers/alibabacloud/services/oss/oss_bucket_secure_transport_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/__init__.py b/tests/providers/alibabacloud/services/ram/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_no_root_access_key/__init__.py b/tests/providers/alibabacloud/services/ram/ram_no_root_access_key/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_lowercase/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_lowercase/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_max_login_attempts/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_max_login_attempts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_max_password_age/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_max_password_age/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_minimum_length/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_minimum_length/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_password_reuse_prevention/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_password_reuse_prevention/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_symbol/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_symbol/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_password_policy_uppercase/__init__.py b/tests/providers/alibabacloud/services/ram/ram_password_policy_uppercase/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/ram/ram_rotate_access_key_90_days/__init__.py b/tests/providers/alibabacloud/services/ram/ram_rotate_access_key_90_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/__init__.py b/tests/providers/alibabacloud/services/rds/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/rds_instance_postgresql_log_connections_enabled/__init__.py b/tests/providers/alibabacloud/services/rds/rds_instance_postgresql_log_connections_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/rds_instance_sql_audit_enabled/__init__.py b/tests/providers/alibabacloud/services/rds/rds_instance_sql_audit_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/rds_instance_sql_audit_retention/__init__.py b/tests/providers/alibabacloud/services/rds/rds_instance_sql_audit_retention/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/rds_instance_ssl_enabled/__init__.py b/tests/providers/alibabacloud/services/rds/rds_instance_ssl_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/rds_instance_tde_enabled/__init__.py b/tests/providers/alibabacloud/services/rds/rds_instance_tde_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/rds/rds_instance_tde_key_custom/__init__.py b/tests/providers/alibabacloud/services/rds/rds_instance_tde_key_custom/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/securitycenter/__init__.py b/tests/providers/alibabacloud/services/securitycenter/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/securitycenter/securitycenter_all_assets_agent_installed/__init__.py b/tests/providers/alibabacloud/services/securitycenter/securitycenter_all_assets_agent_installed/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/securitycenter/securitycenter_vulnerability_scan_enabled/__init__.py b/tests/providers/alibabacloud/services/securitycenter/securitycenter_vulnerability_scan_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/sls/__init__.py b/tests/providers/alibabacloud/services/sls/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/sls/sls_logstore_retention_period/__init__.py b/tests/providers/alibabacloud/services/sls/sls_logstore_retention_period/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/sls/sls_management_console_authentication_failures_alert_enabled/__init__.py b/tests/providers/alibabacloud/services/sls/sls_management_console_authentication_failures_alert_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/sls/sls_management_console_signin_without_mfa_alert_enabled/__init__.py b/tests/providers/alibabacloud/services/sls/sls_management_console_signin_without_mfa_alert_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/sls/sls_root_account_usage_alert_enabled/__init__.py b/tests/providers/alibabacloud/services/sls/sls_root_account_usage_alert_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/sls/sls_unauthorized_api_calls_alert_enabled/__init__.py b/tests/providers/alibabacloud/services/sls/sls_unauthorized_api_calls_alert_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/vpc/__init__.py b/tests/providers/alibabacloud/services/vpc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/alibabacloud/services/vpc/vpc_flow_logs_enabled/__init__.py b/tests/providers/alibabacloud/services/vpc/vpc_flow_logs_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/lib/cloudtrail_timeline/__init__.py b/tests/providers/aws/lib/cloudtrail_timeline/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py b/tests/providers/aws/services/codebuild/codebuild_project_not_publicly_accessible/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public_test.py b/tests/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public_test.py index aace181328..b6465cde60 100644 --- a/tests/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public_test.py +++ b/tests/providers/aws/services/ec2/ec2_ami_public/ec2_ami_public_test.py @@ -141,3 +141,86 @@ class Test_ec2_ami_public: ) assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] + + @mock_aws + def test_multiple_self_owned_amis_mixed_public_and_private(self): + ec2 = client("ec2", region_name=AWS_REGION_US_EAST_1) + + reservation = ec2.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1) + instance = reservation["Instances"][0] + instance_id = instance["InstanceId"] + + private_image_id = ec2.create_image( + InstanceId=instance_id, + Name="test-private-ami", + Description="this is a private test ami", + )["ImageId"] + public_image_id = ec2.create_image( + InstanceId=instance_id, + Name="test-public-ami", + Description="this is a public test ami", + )["ImageId"] + + image = resource("ec2", region_name=AWS_REGION_US_EAST_1).Image(public_image_id) + image.modify_attribute( + ImageId=public_image_id, + Attribute="launchPermission", + OperationType="add", + UserGroups=["all"], + ) + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.ec2.ec2_ami_public.ec2_ami_public.ec2_client", + new=EC2(aws_provider), + ), + ): + from prowler.providers.aws.services.ec2.ec2_ami_public.ec2_ami_public import ( + ec2_ami_public, + ) + + check = ec2_ami_public() + result = check.execute() + + findings_by_resource_id = { + finding.resource_id: finding for finding in result + } + + assert len(result) == 2 + assert set(findings_by_resource_id) == {private_image_id, public_image_id} + + private_finding = findings_by_resource_id[private_image_id] + assert private_finding.status == "PASS" + assert ( + private_finding.status_extended + == "EC2 AMI test-private-ami is not public." + ) + assert ( + private_finding.resource_arn + == f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:image/{private_image_id}" + ) + assert private_finding.region == AWS_REGION_US_EAST_1 + assert private_finding.resource_tags == [] + + public_finding = findings_by_resource_id[public_image_id] + assert public_finding.status == "FAIL" + assert ( + public_finding.status_extended + == "EC2 AMI test-public-ami is currently public." + ) + assert ( + public_finding.resource_arn + == f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:image/{public_image_id}" + ) + assert public_finding.region == AWS_REGION_US_EAST_1 + assert public_finding.resource_tags == [] diff --git a/tests/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled_test.py b/tests/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled_test.py index 8a1f323409..9d19211ca8 100644 --- a/tests/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled_test.py +++ b/tests/providers/aws/services/ec2/ec2_instance_account_imdsv2_enabled/ec2_instance_account_imdsv2_enabled_test.py @@ -1,15 +1,64 @@ +from types import SimpleNamespace from unittest import mock from moto import mock_aws from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, + AWS_COMMERCIAL_PARTITION, + AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1, set_mocked_aws_provider, ) class Test_ec2_instance_account_imdsv2_enabled: + @mock_aws + def test_ec2_imdsv2_uses_region_in_resource_arn(self): + from prowler.providers.aws.services.ec2.ec2_service import ( + InstanceMetadataDefaults, + ) + + ec2_client = SimpleNamespace( + instance_metadata_defaults=[ + InstanceMetadataDefaults( + http_tokens=None, + instances=True, + region=AWS_REGION_US_EAST_1, + ), + InstanceMetadataDefaults( + http_tokens=None, + instances=True, + region=AWS_REGION_EU_WEST_1, + ), + ], + audited_account=AWS_ACCOUNT_NUMBER, + audited_partition=AWS_COMMERCIAL_PARTITION, + provider=SimpleNamespace(scan_unused_services=False), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_account_imdsv2_enabled.ec2_instance_account_imdsv2_enabled.ec2_client", + new=ec2_client, + ), + ): + from prowler.providers.aws.services.ec2.ec2_instance_account_imdsv2_enabled.ec2_instance_account_imdsv2_enabled import ( + ec2_instance_account_imdsv2_enabled, + ) + + result = ec2_instance_account_imdsv2_enabled().execute() + + assert len(result) == 2 + assert {report.resource_arn for report in result} == { + f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:account", + f"arn:aws:ec2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:account", + } + @mock_aws def test_ec2_imdsv2_required(self): from prowler.providers.aws.services.ec2.ec2_service import ( @@ -23,10 +72,8 @@ class Test_ec2_instance_account_imdsv2_enabled: ) ] ec2_client.audited_account = AWS_ACCOUNT_NUMBER + ec2_client.audited_partition = AWS_COMMERCIAL_PARTITION ec2_client.region = AWS_REGION_US_EAST_1 - ec2_client.account_arn_template = ( - f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:account" - ) with ( mock.patch( @@ -71,12 +118,9 @@ class Test_ec2_instance_account_imdsv2_enabled: ) ] ec2_client.audited_account = AWS_ACCOUNT_NUMBER + ec2_client.audited_partition = AWS_COMMERCIAL_PARTITION ec2_client.region = AWS_REGION_US_EAST_1 - ec2_client.account_arn_template = ( - f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:account" - ) - with ( mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", diff --git a/tests/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami_test.py b/tests/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami_test.py index bcf8a80f98..c90756a77a 100644 --- a/tests/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami_test.py +++ b/tests/providers/aws/services/ec2/ec2_instance_with_outdated_ami/ec2_instance_with_outdated_ami_test.py @@ -1,11 +1,17 @@ from unittest import mock import botocore -from moto import mock_aws +import pytest from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider make_api_call = botocore.client.BaseClient._make_api_call +describe_images_calls = [] + + +@pytest.fixture(autouse=True) +def clear_describe_images_calls(): + describe_images_calls.clear() def mock_make_api_call(self, operation_name, kwarg): @@ -27,16 +33,25 @@ def mock_make_api_call(self, operation_name, kwarg): ] } elif operation_name == "DescribeImages": - if "Owners" in kwarg and kwarg["Owners"] == ["amazon"]: + describe_images_calls.append(kwarg) + if kwarg.get("Owners") == ["self"]: + return {"Images": []} + if kwarg.get("Owners") == ["amazon"]: + raise AssertionError( + "Amazon AMIs must not be fetched with a broad owner lookup" + ) + if kwarg.get("ImageIds") == ["ami-12345678"]: return { "Images": [ { "ImageId": "ami-12345678", "DeprecationTime": "2050-01-01T00:00:00.000Z", "Public": True, + "ImageOwnerAlias": "amazon", } ] } + return {"Images": []} return make_api_call(self, operation_name, kwarg) @@ -59,6 +74,13 @@ def mock_make_api_call_private(self, operation_name, kwarg): ] } elif operation_name == "DescribeImages": + describe_images_calls.append(kwarg) + if kwarg.get("Owners") == ["amazon"]: + raise AssertionError( + "Amazon AMIs must not be fetched with a broad owner lookup" + ) + if kwarg.get("Owners") == ["self"]: + return {"Images": []} return { "Images": [ { @@ -90,16 +112,25 @@ def mock_make_api_call_outdated_ami(self, operation_name, kwarg): ] } elif operation_name == "DescribeImages": - if "Owners" in kwarg and kwarg["Owners"] == ["amazon"]: + describe_images_calls.append(kwarg) + if kwarg.get("Owners") == ["self"]: + return {"Images": []} + if kwarg.get("Owners") == ["amazon"]: + raise AssertionError( + "Amazon AMIs must not be fetched with a broad owner lookup" + ) + if kwarg.get("ImageIds") == ["ami-87654321"]: return { "Images": [ { "ImageId": "ami-87654321", "DeprecationTime": "2022-01-01T00:00:00.000Z", "Public": True, + "ImageOwnerAlias": "amazon", } ] } + return {"Images": []} return make_api_call(self, operation_name, kwarg) @@ -122,12 +153,32 @@ def mock_make_api_call_missing_ami(self, operation_name, kwarg): ] } elif operation_name == "DescribeImages": + describe_images_calls.append(kwarg) + if kwarg.get("Owners") == ["amazon"]: + raise AssertionError( + "Amazon AMIs must not be fetched with a broad owner lookup" + ) + return {"Images": []} + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_no_instances(self, operation_name, kwarg): + if operation_name == "DescribeInstances": + return {"Reservations": []} + elif operation_name == "DescribeImages": + describe_images_calls.append(kwarg) + if kwarg.get("Owners") == ["amazon"]: + raise AssertionError( + "Amazon AMIs must not be fetched with a broad owner lookup" + ) return {"Images": []} return make_api_call(self, operation_name, kwarg) class Test_ec2_instance_with_outdated_ami: - @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_no_instances + ) def test_ec2_no_instances(self): from prowler.providers.aws.services.ec2.ec2_service import EC2 @@ -151,6 +202,7 @@ class Test_ec2_instance_with_outdated_ami: result = check.execute() assert len(result) == 0 + assert not any("ImageIds" in call for call in describe_images_calls) @mock.patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_private @@ -178,6 +230,13 @@ class Test_ec2_instance_with_outdated_ami: result = check.execute() assert len(result) == 0 + assert not any( + call.get("Owners") == ["amazon"] for call in describe_images_calls + ) + assert any( + call.get("ImageIds") == ["ami-12345678"] + for call in describe_images_calls + ) @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_instance_ami_not_outdated(self): @@ -209,6 +268,13 @@ class Test_ec2_instance_with_outdated_ami: result[0].status_extended == "EC2 Instance i-0123456789abcdef0 is not using an outdated AMI." ) + assert not any( + call.get("Owners") == ["amazon"] for call in describe_images_calls + ) + assert any( + call.get("ImageIds") == ["ami-12345678"] + for call in describe_images_calls + ) @mock.patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_outdated_ami @@ -242,6 +308,13 @@ class Test_ec2_instance_with_outdated_ami: result[0].status_extended == "EC2 Instance i-0123456789abcdef0 is using outdated AMI ami-87654321." ) + assert not any( + call.get("Owners") == ["amazon"] for call in describe_images_calls + ) + assert any( + call.get("ImageIds") == ["ami-87654321"] + for call in describe_images_calls + ) @mock.patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_missing_ami @@ -269,3 +342,10 @@ class Test_ec2_instance_with_outdated_ami: result = check.execute() assert result == [] + assert not any( + call.get("Owners") == ["amazon"] for call in describe_images_calls + ) + assert any( + call.get("ImageIds") == ["ami-missing"] + for call in describe_images_calls + ) diff --git a/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py b/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py index f0c6eb13e7..d435d971d8 100644 --- a/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py +++ b/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py @@ -39,6 +39,12 @@ def mock_make_api_call(self, operation_name, kwarg): return make_api_call(self, operation_name, kwarg) +def get_mocked_ec2_client(): + ec2_client = mock.MagicMock() + ec2_client.audit_config = {} + return ec2_client + + class Test_ec2_launch_template_no_public_ip: @mock_aws def test_no_launch_templates(self): @@ -124,7 +130,7 @@ class Test_ec2_launch_template_no_public_ip: assert result[0].resource_tags == [] def test_launch_template_public_ip_auto_assign(self): - ec2_client = mock.MagicMock() + ec2_client = get_mocked_ec2_client() launch_template_name = "tester" launch_template_id = "lt-1234567890" launch_template_arn = ( @@ -190,7 +196,7 @@ class Test_ec2_launch_template_no_public_ip: def test_network_interface_with_public_ipv4_network_interface_autoassign_true_and_false( self, ): - ec2_client = mock.MagicMock() + ec2_client = get_mocked_ec2_client() launch_template_name = "tester" launch_template_id = "lt-1234567890" launch_template_arn = ( @@ -290,7 +296,7 @@ class Test_ec2_launch_template_no_public_ip: def test_network_interface_with_public_ipv6_network_interface_autoassign_true_and_false( self, ): - ec2_client = mock.MagicMock() + ec2_client = get_mocked_ec2_client() launch_template_name = "tester" launch_template_id = "lt-1234567890" launch_template_arn = ( diff --git a/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port_from_ip/__init__.py b/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port_from_ip/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/ec2/ec2_service_test.py b/tests/providers/aws/services/ec2/ec2_service_test.py index 1302952e95..3fe154b536 100644 --- a/tests/providers/aws/services/ec2/ec2_service_test.py +++ b/tests/providers/aws/services/ec2/ec2_service_test.py @@ -6,12 +6,17 @@ from datetime import datetime import botocore import mock from boto3 import client, resource +from botocore.exceptions import ClientError from dateutil.tz import tzutc from freezegun import freeze_time from moto import mock_aws from prowler.config.config import encoding_format_utf_8 -from prowler.providers.aws.services.ec2.ec2_service import EC2, Snapshot +from prowler.providers.aws.services.ec2.ec2_service import ( + DESCRIBE_IMAGES_IMAGE_IDS_BATCH_SIZE, + EC2, + Snapshot, +) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1, @@ -196,6 +201,123 @@ class Test_EC2_Service: assert ec2.volumes_with_snapshots == {"vol-old": True, "vol-new": True} assert [snapshot.id for snapshot in ec2.snapshots] == ["snap-new"] + def test_describe_images_by_id_preserves_valid_amis_when_batch_has_missing_ids( + self, + ): + missing_image_id = "ami-0000-missing" + valid_image_ids = [ + f"ami-{index:04d}" + for index in range(1, DESCRIBE_IMAGES_IMAGE_IDS_BATCH_SIZE + 1) + ] + instance_image_ids = valid_image_ids + [missing_image_id] + + class FakeInstance: + def __init__(self, image_id): + self.region = AWS_REGION_US_EAST_1 + self.image_id = image_id + + class FakeEC2Client: + region = AWS_REGION_US_EAST_1 + + def __init__(self): + self.image_id_calls = [] + + def describe_images(self, **kwargs): + if kwargs.get("Owners") == ["self"]: + return {"Images": []} + + image_ids = kwargs["ImageIds"] + self.image_id_calls.append(image_ids) + if missing_image_id in image_ids: + raise ClientError( + { + "Error": { + "Code": "InvalidAMIID.NotFound", + "Message": "The image id does not exist", + } + }, + "DescribeImages", + ) + + return { + "Images": [ + { + "ImageId": image_id, + "Public": True, + "ImageOwnerAlias": "amazon", + } + for image_id in image_ids + ] + } + + regional_client = FakeEC2Client() + ec2 = EC2.__new__(EC2) + ec2.instances = [FakeInstance(image_id) for image_id in instance_image_ids] + ec2.images = [] + ec2.images_by_id = {} + ec2.audit_resources = [] + ec2.audited_partition = "aws" + ec2.audited_account = AWS_ACCOUNT_NUMBER + + ec2._describe_images(regional_client) + + assert ( + len(regional_client.image_id_calls[0]) + == DESCRIBE_IMAGES_IMAGE_IDS_BATCH_SIZE + ) + assert regional_client.image_id_calls[-1] == [valid_image_ids[-1]] + assert missing_image_id not in ec2.images_by_id + assert set(ec2.images_by_id) == set(valid_image_ids) + assert {image.owner for image in ec2.images} == {"amazon"} + + def test_describe_images_ignores_non_amazon_public_images_returned_by_image_id( + self, + ): + marketplace_image_id = "ami-marketplace-public" + vendor_image_id = "ami-vendor-public" + + class FakeInstance: + def __init__(self, image_id): + self.region = AWS_REGION_US_EAST_1 + self.image_id = image_id + + class FakeEC2Client: + region = AWS_REGION_US_EAST_1 + + def describe_images(self, **kwargs): + if kwargs.get("Owners") == ["self"]: + return {"Images": []} + + return { + "Images": [ + { + "ImageId": marketplace_image_id, + "Public": True, + "ImageOwnerAlias": "aws-marketplace", + }, + { + "ImageId": vendor_image_id, + "Public": True, + }, + ] + } + + ec2 = EC2.__new__(EC2) + ec2.instances = [ + FakeInstance(marketplace_image_id), + FakeInstance(vendor_image_id), + ] + ec2.images = [] + ec2.images_by_id = {} + ec2.audit_resources = [] + ec2.audited_partition = "aws" + ec2.audited_account = AWS_ACCOUNT_NUMBER + + ec2._describe_images(FakeEC2Client()) + + assert ec2.images == [] + assert ec2.images_by_id == {} + # Test EC2 Describe Instances @mock_aws @freeze_time(MOCK_DATETIME) @@ -743,9 +865,10 @@ class Test_EC2_Service: {"Key": "OS_Version", "Value": "AWS Linux 2"}, ] - # Verify that Amazon images are also present - amazon_images = [img for img in ec2.images if img.owner == "amazon"] - assert len(amazon_images) > 0 # Should have Amazon AMIs + # Amazon public AMIs are fetched by targeted instance ImageIds only when + # AWS identifies them as Amazon-owned. Moto does not expose that owner + # alias for its fixture AMIs, so this service test only verifies the + # self-owned AMI behavior used by ec2_ami_public. # Test EC2 Describe Volumes @mock_aws diff --git a/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/__init__.py b/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/__init__.py b/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/s3/s3_bucket_object_public/__init__.py b/tests/providers/aws/services/s3/s3_bucket_object_public/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/storagegateway/__init__.py b/tests/providers/aws/services/storagegateway/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/aws/services/waf/waf_regional_webacl_logging_enabled/__init__.py b/tests/providers/aws/services/waf/waf_regional_webacl_logging_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/azure/services/apim/__init__.py b/tests/providers/azure/services/apim/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/azure/services/apim/apim_threat_detection_llm_jacking/__init__.py b/tests/providers/azure/services/apim/apim_threat_detection_llm_jacking/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/azure/services/recovery/__init__.py b/tests/providers/azure/services/recovery/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/external/__init__.py b/tests/providers/external/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudfunction/__init__.py b/tests/providers/gcp/services/cloudfunction/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudfunction/cloudfunction_function_inside_vpc/__init__.py b/tests/providers/gcp/services/cloudfunction/cloudfunction_function_inside_vpc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudfunction/cloudfunction_function_not_publicly_accessible/__init__.py b/tests/providers/gcp/services/cloudfunction/cloudfunction_function_not_publicly_accessible/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/__init__.py b/tests/providers/gcp/services/cloudsql/cloudsql_instance_cmek_encryption_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudsql/cloudsql_instance_high_availability_enabled/__init__.py b/tests/providers/gcp/services/cloudsql/cloudsql_instance_high_availability_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudstorage/cloudstorage_audit_logs_enabled/__init__.py b/tests/providers/gcp/services/cloudstorage/cloudstorage_audit_logs_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/__init__.py b/tests/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/compute/compute_automatic_restart_enabled/__init__.py b/tests/providers/gcp/services/compute/compute_automatic_restart_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/compute/compute_instance_group_multiple_zones/__init__.py b/tests/providers/gcp/services/compute/compute_instance_group_multiple_zones/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/secretmanager/__init__.py b/tests/providers/gcp/services/secretmanager/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/secretmanager/secretmanager_secret_not_publicly_accessible/__init__.py b/tests/providers/gcp/services/secretmanager/secretmanager_secret_not_publicly_accessible/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py b/tests/providers/gcp/services/secretmanager/secretmanager_secret_rotation_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/image/lib/__init__.py b/tests/providers/image/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/image/lib/registry/__init__.py b/tests/providers/image/lib/registry/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/m365/services/defenderidentity/__init__.py b/tests/providers/m365/services/defenderidentity/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/m365/services/defenderidentity/defenderidentity_health_issues_no_open/__init__.py b/tests/providers/m365/services/defenderidentity/defenderidentity_health_issues_no_open/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/m365/services/defenderxdr/__init__.py b/tests/providers/m365/services/defenderxdr/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/m365/services/defenderxdr/defenderxdr_endpoint_privileged_user_exposed_credentials/__init__.py b/tests/providers/m365/services/defenderxdr/defenderxdr_endpoint_privileged_user_exposed_credentials/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_mdm_compliant_device_required/__init__.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_mdm_compliant_device_required/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/m365/services/intune/__init__.py b/tests/providers/m365/services/intune/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/openstack/lib/__init__.py b/tests/providers/openstack/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/openstack/lib/arguments/__init__.py b/tests/providers/openstack/lib/arguments/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/openstack/lib/mutelist/__init__.py b/tests/providers/openstack/lib/mutelist/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/openstack/lib/mutelist/fixtures/__init__.py b/tests/providers/openstack/lib/mutelist/fixtures/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/__init__.py b/tests/providers/oraclecloud/__init__.py deleted file mode 100644 index 45e52625a3..0000000000 --- a/tests/providers/oraclecloud/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# OCI Provider Tests diff --git a/tests/providers/oraclecloud/lib/__init__.py b/tests/providers/oraclecloud/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/lib/mutelist/__init__.py b/tests/providers/oraclecloud/lib/mutelist/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/__init__.py b/tests/providers/oraclecloud/services/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/analytics/__init__.py b/tests/providers/oraclecloud/services/analytics/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/analytics/analytics_instance_access_restricted/__init__.py b/tests/providers/oraclecloud/services/analytics/analytics_instance_access_restricted/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/audit/__init__.py b/tests/providers/oraclecloud/services/audit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/audit/audit_log_retention_period_365_days/__init__.py b/tests/providers/oraclecloud/services/audit/audit_log_retention_period_365_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/blockstorage/__init__.py b/tests/providers/oraclecloud/services/blockstorage/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/blockstorage/blockstorage_block_volume_encrypted_with_cmk/__init__.py b/tests/providers/oraclecloud/services/blockstorage/blockstorage_block_volume_encrypted_with_cmk/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/blockstorage/blockstorage_boot_volume_encrypted_with_cmk/__init__.py b/tests/providers/oraclecloud/services/blockstorage/blockstorage_boot_volume_encrypted_with_cmk/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/cloudguard/__init__.py b/tests/providers/oraclecloud/services/cloudguard/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/cloudguard/cloudguard_enabled/__init__.py b/tests/providers/oraclecloud/services/cloudguard/cloudguard_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/compute/__init__.py b/tests/providers/oraclecloud/services/compute/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/compute/compute_instance_in_transit_encryption_enabled/__init__.py b/tests/providers/oraclecloud/services/compute/compute_instance_in_transit_encryption_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/compute/compute_instance_legacy_metadata_endpoint_disabled/__init__.py b/tests/providers/oraclecloud/services/compute/compute_instance_legacy_metadata_endpoint_disabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/compute/compute_instance_secure_boot_enabled/__init__.py b/tests/providers/oraclecloud/services/compute/compute_instance_secure_boot_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/database/__init__.py b/tests/providers/oraclecloud/services/database/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/database/database_autonomous_database_access_restricted/__init__.py b/tests/providers/oraclecloud/services/database/database_autonomous_database_access_restricted/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/__init__.py b/tests/providers/oraclecloud/services/events/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_notification_topic_and_subscription_exists/__init__.py b/tests/providers/oraclecloud/services/events/events_notification_topic_and_subscription_exists/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_cloudguard_problems/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_cloudguard_problems/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_iam_group_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_iam_group_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_iam_policy_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_iam_policy_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_identity_provider_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_identity_provider_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_idp_group_mapping_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_idp_group_mapping_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_local_user_authentication/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_local_user_authentication/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_network_gateway_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_network_gateway_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_network_security_group_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_network_security_group_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_route_table_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_route_table_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_security_list_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_security_list_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_user_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_user_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/events/events_rule_vcn_changes/__init__.py b/tests/providers/oraclecloud/services/events/events_rule_vcn_changes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/filestorage/__init__.py b/tests/providers/oraclecloud/services/filestorage/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/filestorage/filestorage_file_system_encrypted_with_cmk/__init__.py b/tests/providers/oraclecloud/services/filestorage/filestorage_file_system_encrypted_with_cmk/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/__init__.py b/tests/providers/oraclecloud/services/identity/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_iam_admins_cannot_update_tenancy_admins/__init__.py b/tests/providers/oraclecloud/services/identity/identity_iam_admins_cannot_update_tenancy_admins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_instance_principal_used/__init__.py b/tests/providers/oraclecloud/services/identity/identity_instance_principal_used/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_no_resources_in_root_compartment/__init__.py b/tests/providers/oraclecloud/services/identity/identity_no_resources_in_root_compartment/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_non_root_compartment_exists/__init__.py b/tests/providers/oraclecloud/services/identity/identity_non_root_compartment_exists/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/__init__.py b/tests/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/__init__.py b/tests/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/__init__.py b/tests/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_service_level_admins_exist/__init__.py b/tests/providers/oraclecloud/services/identity/identity_service_level_admins_exist/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_tenancy_admin_permissions_limited/__init__.py b/tests/providers/oraclecloud/services/identity/identity_tenancy_admin_permissions_limited/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_tenancy_admin_users_no_api_keys/__init__.py b/tests/providers/oraclecloud/services/identity/identity_tenancy_admin_users_no_api_keys/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_user_api_keys_rotated_90_days/__init__.py b/tests/providers/oraclecloud/services/identity/identity_user_api_keys_rotated_90_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_user_auth_tokens_rotated_90_days/__init__.py b/tests/providers/oraclecloud/services/identity/identity_user_auth_tokens_rotated_90_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_user_customer_secret_keys_rotated_90_days/__init__.py b/tests/providers/oraclecloud/services/identity/identity_user_customer_secret_keys_rotated_90_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_user_db_passwords_rotated_90_days/__init__.py b/tests/providers/oraclecloud/services/identity/identity_user_db_passwords_rotated_90_days/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_user_mfa_enabled_console_access/__init__.py b/tests/providers/oraclecloud/services/identity/identity_user_mfa_enabled_console_access/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/identity/identity_user_valid_email_address/__init__.py b/tests/providers/oraclecloud/services/identity/identity_user_valid_email_address/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/integration/__init__.py b/tests/providers/oraclecloud/services/integration/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/integration/integration_instance_access_restricted/__init__.py b/tests/providers/oraclecloud/services/integration/integration_instance_access_restricted/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/kms/__init__.py b/tests/providers/oraclecloud/services/kms/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/kms/kms_key_rotation_enabled/__init__.py b/tests/providers/oraclecloud/services/kms/kms_key_rotation_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/logging/__init__.py b/tests/providers/oraclecloud/services/logging/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/__init__.py b/tests/providers/oraclecloud/services/network/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/network_default_security_list_restricts_traffic/__init__.py b/tests/providers/oraclecloud/services/network/network_default_security_list_restricts_traffic/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/network_security_group_ingress_from_internet_to_rdp_port/__init__.py b/tests/providers/oraclecloud/services/network/network_security_group_ingress_from_internet_to_rdp_port/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/network_security_group_ingress_from_internet_to_ssh_port/__init__.py b/tests/providers/oraclecloud/services/network/network_security_group_ingress_from_internet_to_ssh_port/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/network_security_list_ingress_from_internet_to_rdp_port/__init__.py b/tests/providers/oraclecloud/services/network/network_security_list_ingress_from_internet_to_rdp_port/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/network_security_list_ingress_from_internet_to_ssh_port/__init__.py b/tests/providers/oraclecloud/services/network/network_security_list_ingress_from_internet_to_ssh_port/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/network/network_vcn_subnet_flow_logs_enabled/__init__.py b/tests/providers/oraclecloud/services/network/network_vcn_subnet_flow_logs_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/objectstorage/__init__.py b/tests/providers/oraclecloud/services/objectstorage/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_encrypted_with_cmk/__init__.py b/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_encrypted_with_cmk/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_logging_enabled/__init__.py b/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_logging_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_not_publicly_accessible/__init__.py b/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_not_publicly_accessible/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_versioning_enabled/__init__.py b/tests/providers/oraclecloud/services/objectstorage/objectstorage_bucket_versioning_enabled/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index e9442e920f..82682da883 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -4,8 +4,7 @@ All notable changes to the **Prowler UI** are documented in this file. - -## [1.34.0] (Prowler v5.33.1) +## [1.33.1] (Prowler v5.33.1) ### πŸ”„ Changed @@ -17,6 +16,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Jira dispatch polling now reports failed issue creation tasks instead of treating partial failures as successful [(#11925)](https://github.com/prowler-cloud/prowler/pull/11925) --- + ## [1.33.0] (Prowler v5.33.0) ### πŸš€ Added diff --git a/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx index 2e94daf4c7..62bb053f3a 100644 --- a/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx +++ b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx @@ -3,16 +3,22 @@ import { describe, expect, it } from "vitest"; import { LighthouseOverviewBanner } from "./lighthouse-overview-banner"; +const REMEDIATION_PROMPT = + "Find and guide me to remediate which actually matters. What do I have to do today to be secure?"; +const REMEDIATION_HREF = `/lighthouse?prompt=${encodeURIComponent( + REMEDIATION_PROMPT, +)}` as const; + describe("LighthouseOverviewBanner", () => { it("renders Toni copy and links to Lighthouse when connected", () => { // Given / When - render(); + render(); // Then const link = screen.getByRole("link", { name: /Find and remediate which actually matters\./, }); - expect(link).toHaveAttribute("href", "/lighthouse"); + expect(link).toHaveAttribute("href", REMEDIATION_HREF); expect(link).toHaveTextContent("Lighthouse AI"); expect(link).toHaveTextContent( "Find and remediate which actually matters.", diff --git a/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts index 2ac8bcb2df..7668cfafac 100644 --- a/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts +++ b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts @@ -7,9 +7,14 @@ import type { import { getLighthouseOverviewBannerHref, + LIGHTHOUSE_OVERVIEW_PROMPT, resolveLighthouseOverviewBannerHref, } from "./lighthouse-banner"; +const LIGHTHOUSE_OVERVIEW_CHAT_HREF = `/lighthouse?prompt=${encodeURIComponent( + LIGHTHOUSE_OVERVIEW_PROMPT, +)}`; + describe("resolveLighthouseOverviewBannerHref", () => { it("routes to Lighthouse chat when any v2 configuration is connected", () => { // Given / When @@ -19,7 +24,7 @@ describe("resolveLighthouseOverviewBannerHref", () => { ]); // Then - expect(href).toBe("/lighthouse"); + expect(href).toBe(LIGHTHOUSE_OVERVIEW_CHAT_HREF); }); it("routes to Lighthouse settings when no v2 configuration is connected", () => { @@ -82,7 +87,7 @@ describe("getLighthouseOverviewBannerHref", () => { ); // Then - expect(href).toBe("/lighthouse"); + expect(href).toBe(LIGHTHOUSE_OVERVIEW_CHAT_HREF); }); }); diff --git a/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts index 495c2d47d0..a80522bf6c 100644 --- a/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts +++ b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts @@ -2,7 +2,13 @@ import type { LighthouseV2Configuration } from "@/app/(prowler)/lighthouse/_type import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes"; import type { ServerActionResult } from "@/types/server-actions"; -export const LIGHTHOUSE_OVERVIEW_BANNER_HREF = LIGHTHOUSE_ROUTE; +export const LIGHTHOUSE_OVERVIEW_PROMPT = + "Find and guide me to remediate which actually matters. What do I have to do today to be secure?"; + +export const LIGHTHOUSE_OVERVIEW_BANNER_HREF = { + CHAT: `${LIGHTHOUSE_ROUTE.CHAT}?prompt=${encodeURIComponent(LIGHTHOUSE_OVERVIEW_PROMPT)}`, + SETTINGS: LIGHTHOUSE_ROUTE.SETTINGS, +} as const; export type LighthouseOverviewBannerHref = (typeof LIGHTHOUSE_OVERVIEW_BANNER_HREF)[keyof typeof LIGHTHOUSE_OVERVIEW_BANNER_HREF]; diff --git a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx index 85a242bbfe..5642762901 100644 --- a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx +++ b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.test.tsx @@ -248,6 +248,22 @@ describe("LighthouseV2ChatPage", () => { ); }); + it("prefills the overview remediation prompt without starting a conversation", () => { + // Given + const initialPrompt = + "Find and guide me to remediate which actually matters. What do I have to do today to be secure?"; + + // When + renderPage({ initialPrompt }); + + // Then + expect(screen.getByRole("textbox", { name: "Message" })).toHaveValue( + initialPrompt, + ); + expect(createSessionMock).not.toHaveBeenCalled(); + expect(sendMessageMock).not.toHaveBeenCalled(); + }); + it("shows model names in the selector while keeping model ids for persistence", async () => { // Given const user = userEvent.setup(); diff --git a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx index acc50df092..e4941bbb26 100644 --- a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx +++ b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-page.tsx @@ -81,7 +81,6 @@ export function LighthouseV2ChatPage({ initialError, }: LighthouseV2ChatPageProps) { const eventSourceRef = useRef(null); - const initialPromptSentRef = useRef(false); const connectedConfigurations = configurations.filter( (configuration) => configuration.connected === true, ); @@ -99,7 +98,7 @@ export function LighthouseV2ChatPage({ // otherwise keep the first render's activeSessionId. const activeSessionIdRef = useRef(initialSessionId ?? null); const [messages, setMessages] = useState(initialMessages); - const [input, setInput] = useState(""); + const [input, setInput] = useState(initialPrompt ?? ""); const [feedback, setFeedback] = useState(initialError ?? null); const [blockedByConflict, setBlockedByConflict] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); @@ -361,13 +360,6 @@ export function LighthouseV2ChatPage({ return () => closeStream(); }); - useMountEffect(() => { - if (initialPrompt && !initialPromptSentRef.current) { - initialPromptSentRef.current = true; - void submitMessage(initialPrompt); - } - }); - const resetToNewChat = () => { closeStream(); setActiveSessionId(null); diff --git a/ui/app/(prowler)/lighthouse/_components/navigation/index.ts b/ui/app/(prowler)/lighthouse/_components/navigation/index.ts index b3f889b4c9..0e8f765e45 100644 --- a/ui/app/(prowler)/lighthouse/_components/navigation/index.ts +++ b/ui/app/(prowler)/lighthouse/_components/navigation/index.ts @@ -1,2 +1 @@ -export { LighthouseV2NavigationModeSync } from "./lighthouse-v2-navigation-mode-sync"; export { LighthouseV2SidebarChat } from "./lighthouse-v2-sidebar-chat"; diff --git a/ui/app/(prowler)/lighthouse/_components/navigation/lighthouse-v2-navigation-mode-sync.tsx b/ui/app/(prowler)/lighthouse/_components/navigation/lighthouse-v2-navigation-mode-sync.tsx deleted file mode 100644 index b43cccedea..0000000000 --- a/ui/app/(prowler)/lighthouse/_components/navigation/lighthouse-v2-navigation-mode-sync.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use client"; - -import { useMountEffect } from "@/hooks/use-mount-effect"; -import { SIDEBAR_NAVIGATION_MODE, useSidebar } from "@/hooks/use-sidebar"; - -export function LighthouseV2NavigationModeSync() { - const setNavigationMode = useSidebar((state) => state.setNavigationMode); - - useMountEffect(() => { - setNavigationMode(SIDEBAR_NAVIGATION_MODE.CHAT); - }); - - return null; -} diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx index c313a86794..cd3ad44ccf 100644 --- a/ui/app/(prowler)/lighthouse/page.tsx +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -11,11 +11,12 @@ import { getLighthouseV2SupportedProviders, } from "@/app/(prowler)/lighthouse/_actions"; import { LighthouseV2ChatPage } from "@/app/(prowler)/lighthouse/_components/chat"; -import { LighthouseV2NavigationModeSync } from "@/app/(prowler)/lighthouse/_components/navigation"; import { loadLighthouseV2ConnectedModels } from "@/app/(prowler)/lighthouse/_lib/model-loading"; import { LighthouseIcon } from "@/components/icons/Icons"; import { Chat } from "@/components/lighthouse-v1"; import { ContentLayout } from "@/components/shadcn/content-layout"; +import { SidebarNavigationModeSync } from "@/components/sidebar/navigation-mode-sync"; +import { SIDEBAR_NAVIGATION_MODE } from "@/hooks/use-sidebar"; import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes"; import { isCloud } from "@/lib/shared/env"; @@ -77,7 +78,7 @@ export default async function AIChatbot({ return ( }> - + {/* [contain:layout] traps streamdown's fixed fullscreen overlay inside the chat area so it never covers the sidebar or navbar. */}
diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index 4d34d313f1..5111baf656 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -6,6 +6,8 @@ import { getLighthouseV2Configurations } from "@/app/(prowler)/lighthouse/_actio import { ProviderAccountSelectors } from "@/components/filters/provider-account-selectors"; import { ProviderGroupSelector } from "@/components/filters/provider-group-selector"; import { ContentLayout } from "@/components/shadcn/content-layout"; +import { SidebarNavigationModeSync } from "@/components/sidebar/navigation-mode-sync"; +import { SIDEBAR_NAVIGATION_MODE } from "@/hooks/use-sidebar"; import { isCloud } from "@/lib/shared/env"; import { SearchParamsProps } from "@/types"; @@ -53,6 +55,7 @@ export default async function Home({ return ( +
diff --git a/ui/app/(prowler)/profile/page.test.ts b/ui/app/(prowler)/profile/page.test.ts index 2959852393..19184bdf8c 100644 --- a/ui/app/(prowler)/profile/page.test.ts +++ b/ui/app/(prowler)/profile/page.test.ts @@ -9,18 +9,20 @@ describe("profile page layout", () => { const pagePath = path.join(currentDir, "page.tsx"); const source = readFileSync(pagePath, "utf8"); - it("uses one large card with profile sections stacked vertically", () => { + it("places roles before API Keys and exposes its deep-link target", () => { expect(source).toContain('aria-label="User profile settings"'); expect(source).toContain('className="w-full gap-4 p-4 md:p-5"'); + expect(source).toContain('id="api-keys"'); expect(source).not.toContain("xl:grid-cols"); expect(source).not.toContain('className="flex w-full flex-col gap-6"'); const sectionOrder = [ " diff --git a/ui/app/(prowler)/profile/page.tsx b/ui/app/(prowler)/profile/page.tsx index 48f4554db6..8dbd27183f 100644 --- a/ui/app/(prowler)/profile/page.tsx +++ b/ui/app/(prowler)/profile/page.tsx @@ -106,6 +106,11 @@ const SSRDataUser = async ({ > + {hasManageAccount && ( +
+ +
+ )} {hasManageIntegrations && ( )} @@ -115,7 +120,6 @@ const SSRDataUser = async ({ hasManageAccount={hasManageAccount} sessionTenantId={session?.tenantId} /> - {hasManageAccount && } ); }; diff --git a/ui/changelog.d/lighthouse-entry-navigation.fixed.md b/ui/changelog.d/lighthouse-entry-navigation.fixed.md new file mode 100644 index 0000000000..1f74c58894 --- /dev/null +++ b/ui/changelog.d/lighthouse-entry-navigation.fixed.md @@ -0,0 +1 @@ +Lighthouse AI overview entry now starts a new remediation conversation, and returning to Overview restores app navigation mode diff --git a/ui/components/integrations/api-key/api-key-link-card.test.tsx b/ui/components/integrations/api-key/api-key-link-card.test.tsx new file mode 100644 index 0000000000..c0c8c3fe28 --- /dev/null +++ b/ui/components/integrations/api-key/api-key-link-card.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { ApiKeyLinkCard } from "./api-key-link-card"; + +describe("ApiKeyLinkCard", () => { + it("links directly to the API Keys section in the user profile", () => { + // Given / When + render(); + + // Then + expect( + screen.getByRole("link", { name: /go to profile/i }), + ).toHaveAttribute("href", "/profile#api-keys"); + }); +}); diff --git a/ui/components/integrations/api-key/api-key-link-card.tsx b/ui/components/integrations/api-key/api-key-link-card.tsx index 7196b533b9..b54c764f4e 100644 --- a/ui/components/integrations/api-key/api-key-link-card.tsx +++ b/ui/components/integrations/api-key/api-key-link-card.tsx @@ -13,7 +13,7 @@ export const ApiKeyLinkCard = () => { learnMoreUrl="https://docs.prowler.com/user-guide/tutorials/prowler-app-api-keys" learnMoreAriaLabel="Learn more about API Keys" bodyText="API Key management is available in your User Profile. Create and manage API keys to authenticate with the Prowler API for automation and integrations." - linkHref="/profile" + linkHref="/profile#api-keys" linkText="Go to Profile" /> ); diff --git a/ui/components/roles/workflow/forms/add-role-form.test.tsx b/ui/components/roles/workflow/forms/add-role-form.test.tsx index a390b088ac..e5e791fe07 100644 --- a/ui/components/roles/workflow/forms/add-role-form.test.tsx +++ b/ui/components/roles/workflow/forms/add-role-form.test.tsx @@ -137,13 +137,19 @@ describe("AddRoleForm", () => { // Then expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect( - screen.getByText(/tenant-wide visibility setting/i), - ).toHaveTextContent( - /grants visibility into every provider, account, resource, finding, scan, and compliance result.*required to use the Jira integration/i, - ); + screen.getByText( + "Checking the box below grants visibility into every provider: resources, findings, scans, and compliance results, regardless of the provider groups selected.", + ), + ).toBeInTheDocument(); expect( screen.getByText(/required to use the Jira integration/i), ).toHaveProperty("tagName", "STRONG"); + expect( + screen.getByRole("link", { name: /learn more about provider groups/i }), + ).toHaveAttribute( + "href", + "https://docs.prowler.com/user-guide/tutorials/prowler-app-rbac#provider-groups", + ); expect( screen.queryByRole("heading", { name: "Unlimited Visibility" }), ).not.toBeInTheDocument(); @@ -154,12 +160,12 @@ describe("AddRoleForm", () => { ).not.toBeInTheDocument(); expect( screen.queryByText( - /enable it only for roles that need tenant-wide security visibility/i, + /enable it only for roles that need organization-wide security visibility/i, ), ).not.toBeInTheDocument(); expect( screen.queryByText( - /manage providers enables unlimited visibility in this form because provider administration needs tenant-wide provider-group context/i, + /manage providers enables unlimited visibility in this form because provider administration needs organization-wide provider-group context/i, ), ).not.toBeInTheDocument(); @@ -194,7 +200,7 @@ describe("AddRoleForm", () => { }), ).toBeChecked(); expect( - screen.getByText(/tenant-wide visibility setting/i), + screen.getByText(/checking the box below grants visibility/i), ).toBeInTheDocument(); expect(screen.queryByTestId("group-select")).not.toBeInTheDocument(); expect( diff --git a/ui/components/roles/workflow/forms/edit-role-form.test.tsx b/ui/components/roles/workflow/forms/edit-role-form.test.tsx index fe1dd23639..0bc69c8ffd 100644 --- a/ui/components/roles/workflow/forms/edit-role-form.test.tsx +++ b/ui/components/roles/workflow/forms/edit-role-form.test.tsx @@ -131,16 +131,22 @@ describe("EditRoleForm", () => { // Then expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect( - screen.getByText(/tenant-wide visibility setting/i), - ).toHaveTextContent( - /grants visibility into every provider, account, resource, finding, scan, and compliance result.*required to use the Jira integration/i, - ); + screen.getByText( + "Checking the box below grants visibility into every provider: resources, findings, scans, and compliance results, regardless of the provider groups selected.", + ), + ).toBeInTheDocument(); expect( screen.getByText(/required to use the Jira integration/i), ).toHaveProperty("tagName", "STRONG"); + expect( + screen.getByRole("link", { name: /learn more about provider groups/i }), + ).toHaveAttribute( + "href", + "https://docs.prowler.com/user-guide/tutorials/prowler-app-rbac#provider-groups", + ); expect( screen.queryByText( - /manage providers enables unlimited visibility in this form because provider administration needs tenant-wide provider-group context/i, + /manage providers enables unlimited visibility in this form because provider administration needs organization-wide provider-group context/i, ), ).not.toBeInTheDocument(); @@ -181,7 +187,7 @@ describe("EditRoleForm", () => { }), ).toBeChecked(); expect( - screen.getByText(/tenant-wide visibility setting/i), + screen.getByText(/checking the box below grants visibility/i), ).toBeInTheDocument(); expect(screen.queryByTestId("group-select")).not.toBeInTheDocument(); expect( diff --git a/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx b/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx index 50a2e2292d..a8d59d5793 100644 --- a/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx +++ b/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx @@ -2,6 +2,10 @@ import { InfoIcon } from "lucide-react"; import { ReactNode } from "react"; import { Checkbox } from "@/components/shadcn/checkbox/checkbox"; +import { CustomLink } from "@/components/shadcn/custom/custom-link"; + +const PROVIDER_GROUPS_DOCS_URL = + "https://docs.prowler.com/user-guide/tutorials/prowler-app-rbac#provider-groups"; export const UnlimitedVisibilitySection = ({ children, @@ -15,12 +19,20 @@ export const UnlimitedVisibilitySection = ({ aria-hidden="true" className="text-bg-data-info mt-0.5 h-4 w-4 shrink-0" /> -

- This is a tenant-wide visibility setting. It grants visibility into - every provider, account, resource, finding, scan, and compliance - result, regardless of the groups selected below. It is also{" "} - required to use the Jira integration. -

+
+

+ Checking the box below grants visibility into every provider: + resources, findings, scans, and compliance results, regardless of + the provider groups selected. +

+

+ Unlimited Visibility is also{" "} + required to use the Jira integration.{" "} + + Learn more about Provider Groups + +

+
{children}
diff --git a/ui/components/sidebar/navigation-mode-sync.test.tsx b/ui/components/sidebar/navigation-mode-sync.test.tsx new file mode 100644 index 0000000000..935c20762a --- /dev/null +++ b/ui/components/sidebar/navigation-mode-sync.test.tsx @@ -0,0 +1,24 @@ +import { render } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { SIDEBAR_NAVIGATION_MODE, useSidebar } from "@/hooks/use-sidebar"; + +import { SidebarNavigationModeSync } from "./navigation-mode-sync"; + +describe("SidebarNavigationModeSync", () => { + beforeEach(() => { + useSidebar.setState({ + navigationMode: SIDEBAR_NAVIGATION_MODE.CHAT, + }); + }); + + it("restores app navigation mode when the overview mounts", () => { + // Given / When + render(); + + // Then + expect(useSidebar.getState().navigationMode).toBe( + SIDEBAR_NAVIGATION_MODE.BROWSE, + ); + }); +}); diff --git a/ui/components/sidebar/navigation-mode-sync.tsx b/ui/components/sidebar/navigation-mode-sync.tsx new file mode 100644 index 0000000000..d79a596936 --- /dev/null +++ b/ui/components/sidebar/navigation-mode-sync.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useMountEffect } from "@/hooks/use-mount-effect"; +import { type SidebarNavigationMode, useSidebar } from "@/hooks/use-sidebar"; + +interface SidebarNavigationModeSyncProps { + mode: SidebarNavigationMode; +} + +export function SidebarNavigationModeSync({ + mode, +}: SidebarNavigationModeSyncProps) { + const setNavigationMode = useSidebar((state) => state.setNavigationMode); + + useMountEffect(() => { + setNavigationMode(mode); + }); + + return null; +} diff --git a/ui/components/users/profile/memberships-card-client.test.tsx b/ui/components/users/profile/memberships-card-client.test.tsx index 1e00be6071..c5ae7f5e4e 100644 --- a/ui/components/users/profile/memberships-card-client.test.tsx +++ b/ui/components/users/profile/memberships-card-client.test.tsx @@ -67,6 +67,23 @@ const memberships = [ }, ] satisfies MembershipDetailData[]; +const inactiveMembership = { + id: "membership-2", + type: "memberships", + attributes: { + role: "owner", + date_joined: "2026-06-23T10:00:00Z", + }, + relationships: { + tenant: { + data: { + type: "tenants", + id: "tenant-2", + }, + }, + }, +} satisfies MembershipDetailData; + const tenantsMap = { "tenant-1": { id: "tenant-1", @@ -83,6 +100,21 @@ const tenantsMap = { }, }, }, + "tenant-2": { + id: "tenant-2", + type: "tenants", + attributes: { + name: "Prowler Sandbox", + }, + relationships: { + memberships: { + meta: { + count: 1, + }, + data: [], + }, + }, + }, } satisfies Record; describe("MembershipsCardClient", () => { @@ -119,6 +151,25 @@ describe("MembershipsCardClient", () => { expect(cells[2]).toHaveTextContent("Prowler Labs"); }); + it("leaves inactive organization status empty", () => { + // Given / When + render( + , + ); + + // Then + const inactiveRow = screen.getByRole("row", { + name: /owner prowler sandbox/i, + }); + expect(within(inactiveRow).queryByText("Inactive")).not.toBeInTheDocument(); + expect(screen.getByText("Active")).toBeInTheDocument(); + }); + it("keeps organization edit and delete actions inside the actions menu", async () => { // Given const user = userEvent.setup(); diff --git a/ui/components/users/profile/memberships-card-client.tsx b/ui/components/users/profile/memberships-card-client.tsx index c79ba449bc..10b7f28e24 100644 --- a/ui/components/users/profile/memberships-card-client.tsx +++ b/ui/components/users/profile/memberships-card-client.tsx @@ -166,9 +166,7 @@ const membershipColumns: ColumnDef[] = [ cell: ({ row }) => row.original.isActiveTenant ? ( Active - ) : ( - Inactive - ), + ) : null, enableSorting: false, }, { diff --git a/ui/lib/helper.test.ts b/ui/lib/helper.test.ts index f2de6e9145..f59dda487b 100644 --- a/ui/lib/helper.test.ts +++ b/ui/lib/helper.test.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { downloadScanZip, getErrorMessage } from "./helper"; +import { + downloadScanZip, + getErrorMessage, + permissionFormFields, +} from "./helper"; vi.mock("@/actions/scans", () => ({ getComplianceCsv: vi.fn(), @@ -135,3 +139,19 @@ describe("getErrorMessage", () => { ); }); }); + +describe("permissionFormFields", () => { + it("describes Unlimited Visibility as organization-wide", () => { + // Given + const field = permissionFormFields.find( + ({ field }) => field === "unlimited_visibility", + ); + + // When + const description = field?.description; + + // Then + expect(description).toContain("organization-wide visibility"); + expect(description).not.toContain("tenant-wide"); + }); +}); diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts index 692d85771a..e6496c5050 100644 --- a/ui/lib/helper.ts +++ b/ui/lib/helper.ts @@ -438,7 +438,7 @@ export const permissionFormFields: PermissionInfo[] = [ field: "unlimited_visibility", label: "Unlimited Visibility", description: - "Grants tenant-wide visibility across all providers, accounts, resources, findings, scans, and compliance results without granting admin actions.", + "Grants organization-wide visibility across all providers, resources, findings, scans, and compliance results without granting admin actions.", }, { field: "manage_providers", diff --git a/util/update_aws_services_regions.py b/util/update_aws_services_regions.py index 4fc5fc2b8f..40be038723 100644 --- a/util/update_aws_services_regions.py +++ b/util/update_aws_services_regions.py @@ -100,3 +100,4 @@ parsed_matrix_regions_aws = f"{os.path.dirname(os.path.realpath(__name__))}/prow logging.info(f"Writing {parsed_matrix_regions_aws}") with open(parsed_matrix_regions_aws, "w") as outfile: json.dump(regions_by_service, outfile, indent=2, sort_keys=True) + outfile.write("\n")