mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Merge remote-tracking branch 'origin/master' into mintlify/55c1f1a5
This commit is contained in:
@@ -158,7 +158,7 @@ SENTRY_RELEASE=local
|
||||
# REO_DEV_CLIENT_ID=
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.42.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.43.0
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
+13
-13
@@ -1,23 +1,23 @@
|
||||
# SDK
|
||||
/* @prowler-cloud/detection-remediation
|
||||
/prowler/ @prowler-cloud/detection-remediation
|
||||
/tests/ @prowler-cloud/detection-remediation
|
||||
/dashboard/ @prowler-cloud/detection-remediation
|
||||
/docs/ @prowler-cloud/detection-remediation
|
||||
/examples/ @prowler-cloud/detection-remediation
|
||||
/util/ @prowler-cloud/detection-remediation
|
||||
/contrib/ @prowler-cloud/detection-remediation
|
||||
/permissions/ @prowler-cloud/detection-remediation
|
||||
/codecov.yml @prowler-cloud/detection-remediation @prowler-cloud/api
|
||||
/* @prowler-cloud/engineering
|
||||
/prowler/ @prowler-cloud/engineering
|
||||
/tests/ @prowler-cloud/engineering
|
||||
/dashboard/ @prowler-cloud/engineering
|
||||
/docs/ @prowler-cloud/engineering
|
||||
/examples/ @prowler-cloud/engineering
|
||||
/util/ @prowler-cloud/engineering
|
||||
/contrib/ @prowler-cloud/engineering
|
||||
/permissions/ @prowler-cloud/engineering
|
||||
/codecov.yml @prowler-cloud/engineering
|
||||
|
||||
# API
|
||||
/api/ @prowler-cloud/api
|
||||
/api/ @prowler-cloud/engineering
|
||||
|
||||
# UI
|
||||
/ui/ @prowler-cloud/ui
|
||||
/ui/ @prowler-cloud/engineering
|
||||
|
||||
# AI
|
||||
/mcp_server/ @prowler-cloud/detection-remediation
|
||||
/mcp_server/ @prowler-cloud/engineering
|
||||
|
||||
# Platform
|
||||
/.github/ @prowler-cloud/platform
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Check labels
|
||||
id: label_check
|
||||
uses: agilepathway/label-checker@c3d16ad512e7cea5961df85ff2486bb774caf3c5 # v1.6.65
|
||||
uses: agilepathway/label-checker@c324842522fbd012e4f590afe3b4e591301322ed # v1.6.66
|
||||
with:
|
||||
allow_failure: true
|
||||
prefix_mode: true
|
||||
|
||||
@@ -44,7 +44,10 @@ jobs:
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install boto3
|
||||
# Pinned to the versions in pyproject.toml: the ISO partitions region
|
||||
# data comes from the endpoints.json bundled with botocore, so the
|
||||
# botocore version is itself a data source and must be deterministic
|
||||
run: pip install boto3==1.40.61 botocore==1.40.61
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
|
||||
|
||||
+11
@@ -27,6 +27,17 @@ ignore:
|
||||
package:
|
||||
name: google.golang.org/grpc
|
||||
version: v1.82.1
|
||||
# CVE-2026-84445 is the same temporary exception documented in .trivyignore.yaml:
|
||||
# Trivy 0.74.0 still embeds grpc 1.82.1, while the 1.82.2 / 1.83.2 fix is not in any
|
||||
# release. The panic needs a gRPC server built with `xds.NewGRPCServer()`; Prowler only
|
||||
# runs `trivy image` / `trivy fs`, so the image serves no gRPC at all. Pinned to the
|
||||
# embedded version so the rule stops matching on its own once Trivy bumps grpc. Remove
|
||||
# with the Trivy exception by 2026-10-15.
|
||||
# https://github.com/advisories/GHSA-2v4p-qf9q-27wj
|
||||
- vulnerability: CVE-2026-84445
|
||||
package:
|
||||
name: google.golang.org/grpc
|
||||
version: v1.82.1
|
||||
# CVE-2026-56855 / CVE-2026-78662 are the same temporary exception documented in
|
||||
# .trivyignore.yaml: Trivy 0.74.0 still embeds golang.org/x/crypto v0.55.0, while the
|
||||
# 0.56.0 fix (published 2026-09-02) hasn't reached any Trivy release, or even Trivy
|
||||
|
||||
@@ -176,6 +176,25 @@ vulnerabilities:
|
||||
- "pkg:golang/google.golang.org/grpc"
|
||||
expired_at: 2026-10-15
|
||||
|
||||
# CVE-2026-84445 is a DoS in grpc-go servers built with `xds.NewGRPCServer()`: a request
|
||||
# carrying neither `:authority` nor `Host` reaches the xDS routing interceptor, which
|
||||
# indexes an empty slice of authorities and panics. The per-RPC goroutine does not
|
||||
# recover, so the whole server process dies. Fixed in 1.82.2 and 1.83.2 (published
|
||||
# 2026-09-08). Trivy 0.74.0, the latest published release and the version the images
|
||||
# ship, pins 1.82.1 as an indirect dependency:
|
||||
# https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod
|
||||
# Trivy main already carries 1.83.2, but no published release includes it yet.
|
||||
# The reachability argument is the one made for CVE-2026-84304 above, only narrower:
|
||||
# this panic needs an xDS-managed gRPC server. Prowler invokes Trivy exclusively as
|
||||
# `trivy image` and `trivy fs` on a local path, never `trivy server`, so the image runs
|
||||
# no gRPC server at all, xDS or otherwise. Remove this temporary suppression as soon as
|
||||
# a Trivy release pins grpc >= 1.83.2.
|
||||
# https://github.com/advisories/GHSA-2v4p-qf9q-27wj
|
||||
- id: CVE-2026-84445
|
||||
purls:
|
||||
- "pkg:golang/google.golang.org/grpc@v1.82.1"
|
||||
expired_at: 2026-10-15
|
||||
|
||||
# CVE-2026-56855 and CVE-2026-78662 are DoS deadlocks in x/crypto/ssh: a malicious peer
|
||||
# can flood or misuse channel messages (RFC 4254) to block the whole connection.
|
||||
# Fixed in golang.org/x/crypto v0.56.0 (published 2026-09-02). Trivy 0.74.0, the latest
|
||||
|
||||
@@ -4,6 +4,14 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.43.0] (Prowler v5.42.0)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Speed up compliance overview ingestion by reading ThreatScore mappings from the compliance template instead of each finding, generating time-ordered `uuid7` row ids and grouping inserted rows by framework and requirement [(#12738)](https://github.com/prowler-cloud/prowler/pull/12738)
|
||||
|
||||
---
|
||||
|
||||
## [1.42.0] (Prowler v5.41.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Speed up compliance overview ingestion by reading ThreatScore mappings from the compliance template instead of each finding, generating time-ordered `uuid7` row ids and grouping inserted rows by framework and requirement
|
||||
+1
-1
@@ -71,7 +71,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.43.0"
|
||||
version = "1.44.0"
|
||||
|
||||
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
|
||||
# target-version tracks this project's lowest supported Python.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.43.0
|
||||
version: 1.44.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -4938,7 +4938,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.43.0"
|
||||
version = "1.44.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
|
||||
@@ -4,6 +4,62 @@ description: "New features and improvements in each Prowler release"
|
||||
rss: true
|
||||
---
|
||||
|
||||
<Update label="v5.42.0" description="September 11, 2026">
|
||||
### ☁️ AWS — ISO Partitions
|
||||
|
||||
Prowler now resolves regions and services for the AWS ISO partitions (`aws-iso`, `aws-iso-b`, `aws-iso-e` and `aws-iso-f`) the same way it does for the commercial, China, European Sovereign Cloud and GovCloud partitions. The region matrix is filled from the endpoint metadata bundled with botocore, which needs no credentials or network access, so it covers partitions that are air-gapped from the internet. Scanning them no longer requires a hand-edited `aws_regions_by_service.json`: ISO regions such as `us-isob-east-1` are accepted by `--region` and `--excluded-region`.
|
||||
|
||||
Deployments that declare `PROWLER_AWS_PARTITION` also keep their bootstrap STS calls in the configured region when it belongs to that partition. An install in `us-gov-west-1` that reaches AWS only through its own VPC endpoints is no longer sent to `us-gov-east-1`, where the connection check and the scan used to time out.
|
||||
|
||||
Read more in the [AWS Regions and Partitions documentation](https://docs.prowler.com/user-guide/providers/aws/regions-and-partitions).
|
||||
|
||||
### ⏱️ AWS — Configurable Timeouts for Restricted Networks
|
||||
|
||||
Scans from networks with restricted egress (VPC endpoints for only some services, GovCloud or private deployments) could take hours: Boto3 waits 60 seconds to connect by default and retries connection errors, so every service without a reachable endpoint cost up to four 60-second attempts in every region. Prowler now lowers the default connect timeout to 10 seconds, keeps the read timeout at 60 seconds, and exposes both through `--aws-connect-timeout` and `--aws-read-timeout`, or through the `PROWLER_AWS_BOTO3_CONNECT_TIMEOUT` and `PROWLER_AWS_BOTO3_READ_TIMEOUT` environment variables for deployments without a CLI. `--aws-retries-max-attempts 0` now disables retries instead of silently falling back to three, leaving a single attempt per call.
|
||||
|
||||
Read more in the [Boto3 configuration documentation](https://docs.prowler.com/user-guide/providers/aws/boto3-configuration).
|
||||
|
||||
### 🐳 Image Provider — Reusable Vulnerability Database
|
||||
|
||||
The Image provider now honors `TRIVY_CACHE_DIR`. When the variable names a directory, Trivy keeps its vulnerability database there and Prowler leaves the directory in place after the scan, so the database is downloaded once instead of on every scan. Hosts without internet access can now scan images by pointing `TRIVY_CACHE_DIR` at a pre-populated database and setting `TRIVY_SKIP_DB_UPDATE=true`. Without the variable, the temporary cache is created and removed as before.
|
||||
|
||||
Read more in the [Image provider documentation](https://docs.prowler.com/user-guide/providers/image/getting-started-image#vulnerability-database-cache).
|
||||
|
||||
### 🎫 Jira Integration — Faster Connection Test
|
||||
|
||||
Testing a Jira integration no longer reports a false failure on accounts with many projects. The connection test fetched the issue types of every project one request at a time, which could outlast the wait in the UI even when the check was about to succeed. Issue types are now fetched concurrently, a project whose issue types the integration user cannot see is no longer logged as an error, and the Integrations page keeps following the connection test instead of giving up after about a minute.
|
||||
|
||||
Read more in the [Jira integration documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-jira-integration).
|
||||
|
||||
### 📚 Compliance — Catalog Integrity Fixes
|
||||
|
||||
A new integrity test runs over every compliance framework, asserting unique requirement IDs, no check listed twice within a requirement, and that every referenced check exists for its provider. The fixes it drove span 42 frameworks across AWS, Azure, GCP, GitHub, Kubernetes and Microsoft 365:
|
||||
|
||||
- **Duplicate requirement IDs:** identical copies are removed, and distinct requirements that shared an ID get their own, such as `1.10` in CIS AWS 5.0 and `rc_rp_1` for RC.RP-1 in NIST CSF 1.1. In Prowler ThreatScore for Azure, SQL auditing retention moves from `3.2.1` to `3.2.4`, and requirement `1.2.1` of Prowler ThreatScore for GCP now points to `iam_sa_no_user_managed_keys`.
|
||||
- **Stale check references:** checks that no longer exist are replaced with their current name when there is a direct equivalent, or removed so the requirement reports as manual. Most of these were in the FedRAMP 20x KSI frameworks.
|
||||
|
||||
Renamed requirement IDs appear as new requirements for scans run after the upgrade.
|
||||
|
||||
The compliance overview task that runs after every scan is also faster: ThreatScore mappings are read once from the compliance template instead of from every finding, and rows are inserted with time-ordered `uuid7` IDs grouped by framework and requirement.
|
||||
|
||||
Read more in the [Compliance documentation](https://docs.prowler.com/user-guide/compliance/tutorials/compliance).
|
||||
|
||||
### 🔍 Checks
|
||||
|
||||
`rolesanywhere_profile_restricts_session_permissions`, `iam_role_service_trust_restricts_source_to_account` and `codebuild_project_uses_allowed_github_organizations` no longer crash with `TypeError` when the scanning role is denied `iam:ListRoles`, which dropped every finding of those checks for the account. Without the role inventory, an enabled IAM Roles Anywhere profile without session scoping reports `MANUAL`, and CodeBuild projects whose service role cannot be resolved are skipped.
|
||||
|
||||
Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
|
||||
|
||||
### 🔐 Security Updates
|
||||
|
||||
- `next` upgraded to 16.3.3 in the UI, patching unauthenticated remote code execution through AVIF image optimization ([GHSA-2xp9-vwfh-vxw4](https://github.com/advisories/GHSA-2xp9-vwfh-vxw4)) and on Windows-hosted servers ([GHSA-p293-qw3h-jr36](https://github.com/advisories/GHSA-p293-qw3h-jr36)).
|
||||
- `sharp` upgraded to 0.35.4 in the UI, patching libheif image-decoding vulnerabilities ([GHSA-rgj7-g3m4-5g8c](https://github.com/advisories/GHSA-rgj7-g3m4-5g8c)).
|
||||
- `nanoid`, `js-yaml` and `postcss`, plus eleven transitive UI dependencies, upgraded to patched versions, resolving 40 npm audit advisories (21 high, 15 moderate, 4 low).
|
||||
- `libuuid` upgraded to 2.41.6-r1 in the MCP Server image, patching CVE-2026-53612, CVE-2026-53613, CVE-2026-53614, CVE-2026-76642, CVE-2026-78408 and CVE-2026-78410.
|
||||
|
||||
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.42.0) for the complete list of changes.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.41.0" description="September 2, 2026">
|
||||
### 📥 Scans — Import Findings from the Browser
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
title: 'Basic Usage'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
## Running Prowler
|
||||
|
||||
Running Prowler requires specifying the provider (e.g. `aws`, `gcp`, `azure`, `kubernetes`, `m365`, `github`, `iac` or `mongodbatlas`):
|
||||
@@ -91,6 +93,18 @@ By default, `prowler` will scan all AWS regions.
|
||||
</Note>
|
||||
See more details about AWS Authentication in the [Authentication Section](/user-guide/providers/aws/authentication) section.
|
||||
|
||||
- **AWS Retrier and Timeout Configuration**
|
||||
|
||||
<VersionBadge version="5.42.0" />
|
||||
|
||||
Tune the Boto3 standard retrier and the endpoint timeouts when AWS throttles the scan or when some endpoints are unreachable from the network Prowler runs in:
|
||||
|
||||
```console
|
||||
prowler aws --aws-retries-max-attempts 5 --aws-connect-timeout 5 --aws-read-timeout 30
|
||||
```
|
||||
|
||||
See the [Boto3 configuration](/user-guide/providers/aws/boto3-configuration) page for defaults and environment variables.
|
||||
|
||||
## Azure
|
||||
|
||||
Azure requires specifying the auth method:
|
||||
|
||||
@@ -128,8 +128,8 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.41.0"
|
||||
PROWLER_API_VERSION="5.41.0"
|
||||
PROWLER_UI_VERSION="5.42.0"
|
||||
PROWLER_API_VERSION="5.42.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
---
|
||||
title: "Boto3 Retrier Configuration in Prowler"
|
||||
title: "Boto3 Retrier and Timeout Configuration in Prowler"
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
Prowler's AWS Provider leverages Boto3's [Standard](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html) retry mode to automatically retry client calls to AWS services when encountering errors or exceptions.
|
||||
|
||||
## Timeout Configuration
|
||||
|
||||
<VersionBadge version="5.42.0" />
|
||||
|
||||
Every AWS API call is bounded by two timeouts:
|
||||
|
||||
- Connect timeout: seconds to wait to establish a connection (TCP, proxy tunnel and TLS handshake) to the AWS endpoint. Prowler's default is 10 seconds, configurable via `--aws-connect-timeout 5`.
|
||||
- Read timeout: seconds to wait for a response once connected. Prowler's default is 60 seconds, configurable via `--aws-read-timeout 30`.
|
||||
|
||||
Both timeouts can also be set through environment variables, which is the way to tune them in Prowler Cloud and other deployments without a CLI:
|
||||
|
||||
```console
|
||||
export PROWLER_AWS_BOTO3_CONNECT_TIMEOUT=5
|
||||
export PROWLER_AWS_BOTO3_READ_TIMEOUT=30
|
||||
```
|
||||
|
||||
CLI flags take precedence over the environment variables. Prowler sets both timeouts explicitly, so `AWS_DEFAULTS_MODE` and a `connect_timeout` in `~/.aws/config` are ignored; use the flag or the environment variable instead.
|
||||
|
||||
<Note>
|
||||
Boto3 defaults both timeouts to 60 seconds. In networks with restricted egress (for example VPC endpoints for a subset of services, GovCloud or private deployments), every AWS service without a reachable endpoint used to cost up to 4 attempts × 60 seconds (the first call plus the 3 retries) for each region. Prowler lowers the connect timeout to 10 seconds so unreachable endpoints fail fast; lower it further together with `--aws-retries-max-attempts 0`, which disables retries and leaves a single attempt per call, if a scan still spends most of its time waiting on unreachable services.
|
||||
|
||||
</Note>
|
||||
|
||||
## Retry Behavior Overview
|
||||
|
||||
Boto3's Standard retry mode includes the following mechanisms:
|
||||
|
||||
- Maximum Retry Attempts: Default value set to 3, configurable via the `--aws-retries-max-attempts 5` argument.
|
||||
- Maximum Retry Attempts: Default value set to 3, configurable via the `--aws-retries-max-attempts 5` argument. `0` disables retries.
|
||||
|
||||
- Expanded Error Handling: Retries occur for a comprehensive set of errors.
|
||||
|
||||
|
||||
@@ -21,10 +21,28 @@ When scanning the China (`aws-cn`), European Sovereign Cloud (`aws-eusc`) or Gov
|
||||
|
||||
- Specify the regions to audit within that partition using the `-f/--region` flag.
|
||||
|
||||
- Declare the partition with the `PROWLER_AWS_PARTITION` environment variable, set to `aws`, `aws-cn`, `aws-eusc` or `aws-us-gov`.
|
||||
|
||||
<Note>
|
||||
Refer to: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#configuring-credentials for more information about the AWS credential configuration.
|
||||
|
||||
</Note>
|
||||
### Declaring the Partition
|
||||
|
||||
`PROWLER_AWS_PARTITION` tells Prowler which partition the scan runs against, without relying on a region being configured:
|
||||
|
||||
```bash
|
||||
export PROWLER_AWS_PARTITION="aws-us-gov"
|
||||
```
|
||||
|
||||
It matters most where nothing else says. Resolving an identity means calling STS before anything is known about the credentials, and with no region configured Prowler would otherwise start from the commercial endpoints. Declaring the partition makes that first call go to the right place, which is the difference between a scan that starts and one that fails on an endpoint the credentials cannot use.
|
||||
|
||||
A region configured for the session still wins when it belongs to the declared partition, so a deployment in `us-gov-west-1` is not sent to `us-gov-east-1`. A region belonging to a different partition is ignored, since a partition that has been declared explicitly is the more deliberate statement of the two.
|
||||
|
||||
<Note>
|
||||
Set it wherever the scan runs. For deployments that scan from containers, that means the environment of the containers doing the scanning, not only the one accepting the request.
|
||||
</Note>
|
||||
|
||||
### Scanning Specific Regions
|
||||
|
||||
To scan a particular AWS region with Prowler, use:
|
||||
|
||||
@@ -96,6 +96,29 @@ Install Trivy using one of the following methods:
|
||||
|
||||
For additional installation methods, see the [Trivy installation guide](https://trivy.dev/latest/getting-started/installation/).
|
||||
|
||||
### Vulnerability Database Cache
|
||||
|
||||
<VersionBadge version="5.42.0" />
|
||||
|
||||
Trivy keeps its vulnerability database in a cache directory. By default Prowler gives it a temporary one and removes it when the scan ends, so the database is downloaded again for every scan.
|
||||
|
||||
Set `TRIVY_CACHE_DIR` to a directory that persists and the database is downloaded once and reused:
|
||||
|
||||
```bash
|
||||
export TRIVY_CACHE_DIR="$HOME/.cache/trivy"
|
||||
prowler image --image <image>
|
||||
```
|
||||
|
||||
Prowler never deletes a directory you supply. Trivy still creates and updates its cache and database files inside it.
|
||||
|
||||
<Note>
|
||||
A host with no internet access needs a pre-populated vulnerability database in a persistent directory, with `TRIVY_CACHE_DIR` pointing at it. Populate the directory on a machine that does have access and copy it across.
|
||||
|
||||
Trivy tries to refresh the database when it considers it stale, and that download fails without network access. Set `TRIVY_SKIP_DB_UPDATE=true` (and `TRIVY_SKIP_JAVA_DB_UPDATE=true` if Java scanning is enabled) so it uses the supplied database as is.
|
||||
|
||||
The database ages. A scan run against an old one reports only the vulnerabilities known when it was built, and nothing in the output says so, so keep track of when it was last refreshed.
|
||||
</Note>
|
||||
|
||||
|
||||
### Supported Scanners
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [0.12.1] (Prowler v5.42.0)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- `libuuid` upgraded to 2.41.6-r1 in the container image, patching CVE-2026-53612, CVE-2026-53613, CVE-2026-53614, CVE-2026-76642, CVE-2026-78408 and CVE-2026-78410 [(#12780)](https://github.com/prowler-cloud/prowler/pull/12780)
|
||||
|
||||
---
|
||||
|
||||
## [0.12.0] (Prowler v5.41.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -32,6 +32,8 @@ LABEL maintainer="https://github.com/prowler-cloud"
|
||||
# High CVEs fixed in Alpine 3.23 but not yet in the pinned base image:
|
||||
# sqlite-libs 3.53.4-r0 CVE-2026-11822, CVE-2026-11824 (image ships 3.51.2-r0)
|
||||
# libcrypto3/libssl3 3.5.8-r0 CVE-2026-14456 (image ships 3.5.7-r0)
|
||||
# libuuid 2.41.6-r1 CVE-2026-53612, -53613, -53614, -76642, -78408, -78410
|
||||
# (image ships 2.41.4-r0; -78408 is the one that needs -r1 rather than -r0)
|
||||
# The base image pins python 3.13.14, which has not been rebuilt since those
|
||||
# packages were published, so the upgrade is taken here rather than by moving
|
||||
# the pin -- the newest published python:3.13-alpine3.23 carries the same
|
||||
@@ -43,7 +45,8 @@ LABEL maintainer="https://github.com/prowler-cloud"
|
||||
RUN apk add --no-cache --upgrade \
|
||||
"sqlite-libs>=3.53.4-r0" \
|
||||
"libcrypto3>=3.5.8-r0" \
|
||||
"libssl3>=3.5.8-r0"
|
||||
"libssl3>=3.5.8-r0" \
|
||||
"libuuid>=2.41.6-r1"
|
||||
|
||||
# Create non-root user for security
|
||||
# Using specific UID/GID for consistency across environments
|
||||
|
||||
@@ -4,6 +4,33 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [5.42.0] (Prowler v5.42.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- AWS ISO partitions (`aws-iso`, `aws-iso-b`, `aws-iso-e` and `aws-iso-f`) to the AWS service region matrix, generated from the endpoints data bundled with botocore [(#12759)](https://github.com/prowler-cloud/prowler/pull/12759)
|
||||
- `--aws-connect-timeout` and `--aws-read-timeout` CLI flags, plus `PROWLER_AWS_BOTO3_CONNECT_TIMEOUT` and `PROWLER_AWS_BOTO3_READ_TIMEOUT` environment variables, to bound how long each AWS API call waits for an endpoint [(#12774)](https://github.com/prowler-cloud/prowler/pull/12774)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- AWS provider default Boto3 connect timeout lowered from 60 to 10 seconds, so scans in restricted-egress networks (VPC endpoints for a subset of services, GovCloud, private deployments) no longer spend 4 minutes per region on every service whose endpoint is unreachable [(#12774)](https://github.com/prowler-cloud/prowler/pull/12774)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Duplicate requirement ids, checks listed twice in a requirement and references to non-existent checks across compliance frameworks, now guarded by a catalog integrity test [(#12717)](https://github.com/prowler-cloud/prowler/pull/12717)
|
||||
- Duplicate requirement `3.2.1` in ProwlerThreatScore for Azure (SQL auditing retention is now `3.2.4`) and doubled check id in requirement `1.2.1` of ProwlerThreatScore for GCP [(#12717)](https://github.com/prowler-cloud/prowler/pull/12717)
|
||||
- Jira connection checks no longer log an error when a single project has no issue types visible to the integration user (typically a missing "create issue" permission on that project), a case the caller already treats as non-fatal [(#12742)](https://github.com/prowler-cloud/prowler/pull/12742)
|
||||
- `Jira.test_connection()` now fetches each project's issue types concurrently instead of one request at a time, so accounts with many Jira projects no longer take tens of seconds (unbounded, scaling with the project count) to verify the connection [(#12742)](https://github.com/prowler-cloud/prowler/pull/12742)
|
||||
- `AwsProvider.get_available_aws_service_regions()` now returns an empty set for an unknown service or partition instead of raising `KeyError`, so a service unavailable in the audited partition is skipped [(#12759)](https://github.com/prowler-cloud/prowler/pull/12759)
|
||||
- `AwsProvider.generate_regional_clients()` now returns an empty dict instead of `None` when the regional clients cannot be built, a failure that surfaced later as `AttributeError: 'NoneType' object has no attribute 'values'` [(#12759)](https://github.com/prowler-cloud/prowler/pull/12759)
|
||||
- `AwsProvider.get_global_region()` now returns a real region for each ISO partition instead of the `aws-iso-global` pseudo endpoint, which collapsed the four partitions into one answer [(#12759)](https://github.com/prowler-cloud/prowler/pull/12759)
|
||||
- Bootstrap STS calls now use the session region when `PROWLER_AWS_PARTITION` is set and the region belongs to that partition, instead of always going to the partition's global STS region, which a deployment reached only through its own region's VPC endpoints cannot route to [(#12764)](https://github.com/prowler-cloud/prowler/pull/12764)
|
||||
- The Image provider now uses the directory named by `TRIVY_CACHE_DIR` when one is set, instead of a fresh temporary directory it deletes afterwards, so a deployment can supply a vulnerability database it already holds and one with network access stops re-downloading the database for every image it scans [(#12773)](https://github.com/prowler-cloud/prowler/pull/12773)
|
||||
- `--aws-retries-max-attempts 0` now disables Boto3 retries instead of being silently ignored in favour of the default of 3 [(#12774)](https://github.com/prowler-cloud/prowler/pull/12774)
|
||||
- `rolesanywhere_profile_restricts_session_permissions`, `iam_role_service_trust_restricts_source_to_account` and `codebuild_project_uses_allowed_github_organizations` crashing with `TypeError` when `iam:ListRoles` is denied [(#12785)](https://github.com/prowler-cloud/prowler/pull/12785)
|
||||
|
||||
---
|
||||
|
||||
## [5.41.0] (Prowler v5.41.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Duplicate requirement ids, checks listed twice in a requirement and references to non-existent checks across compliance frameworks, now guarded by a catalog integrity test
|
||||
@@ -1 +0,0 @@
|
||||
`Jira.test_connection()` now fetches each project's issue types concurrently instead of one request at a time, so accounts with many Jira projects no longer take tens of seconds (unbounded, scaling with the project count) to verify the connection
|
||||
@@ -1 +0,0 @@
|
||||
Jira connection checks no longer log an error when a single project has no issue types visible to the integration user (typically a missing "create issue" permission on that project), a case the caller already treats as non-fatal
|
||||
@@ -1 +0,0 @@
|
||||
Duplicate requirement `3.2.1` in ProwlerThreatScore for Azure (SQL auditing retention is now `3.2.4`) and doubled check id in requirement `1.2.1` of ProwlerThreatScore for GCP
|
||||
@@ -52,7 +52,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.42.0"
|
||||
prowler_version = "5.43.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -126,6 +126,8 @@ class AwsProvider(Provider):
|
||||
aws_access_key_id: str = None,
|
||||
aws_secret_access_key: str = None,
|
||||
aws_session_token: Optional[str] = None,
|
||||
connect_timeout: Optional[int] = None,
|
||||
read_timeout: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Initializes the AWS provider.
|
||||
@@ -155,6 +157,8 @@ class AwsProvider(Provider):
|
||||
- aws_access_key_id: The AWS access key ID.
|
||||
- aws_secret_access_key: The AWS secret access key.
|
||||
- aws_session_token: The AWS session token, optional.
|
||||
- connect_timeout: Seconds to wait to establish a connection to an AWS endpoint.
|
||||
- read_timeout: Seconds to wait for a response from an AWS endpoint.
|
||||
|
||||
Raises:
|
||||
- ArgumentTypeError: If the input MFA ARN is invalid.
|
||||
@@ -229,7 +233,9 @@ class AwsProvider(Provider):
|
||||
|
||||
# TODO: Use AwsSetUpSession ?????
|
||||
# Configure the initial AWS Session using the local credentials: profile or environment variables
|
||||
session_config = self.set_session_config(retries_max_attempts)
|
||||
session_config = self.set_session_config(
|
||||
retries_max_attempts, connect_timeout, read_timeout
|
||||
)
|
||||
aws_session = self.setup_session(
|
||||
mfa=mfa,
|
||||
profile=profile,
|
||||
@@ -576,8 +582,15 @@ class AwsProvider(Provider):
|
||||
) -> str:
|
||||
excluded_regions = set(excluded_regions or ())
|
||||
session_region = session.region_name
|
||||
env_partition_regions = get_env_partition_regions(session_region)
|
||||
if session_region and session_region not in excluded_regions:
|
||||
return session_region
|
||||
if not env_partition_regions or session_region in env_partition_regions:
|
||||
return session_region
|
||||
if env_partition_regions:
|
||||
for region in env_partition_regions:
|
||||
if region not in excluded_regions:
|
||||
return region
|
||||
return env_partition_regions[0]
|
||||
|
||||
for region in AwsProvider.get_bootstrap_region_candidates(session_region):
|
||||
if region not in excluded_regions:
|
||||
@@ -673,7 +686,7 @@ class AwsProvider(Provider):
|
||||
session = Session(**session_arguments)
|
||||
session._session.set_default_client_config(session_config)
|
||||
sts_region = (
|
||||
get_env_partition_bootstrap_region()
|
||||
get_env_partition_bootstrap_region(session.region_name)
|
||||
or session.region_name
|
||||
or AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
)
|
||||
@@ -908,6 +921,9 @@ class AwsProvider(Provider):
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
# Return an empty dict, as promised by the signature, so the service
|
||||
# is simply not scanned instead of the callers failing later on a None
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_available_aws_service_regions(
|
||||
@@ -923,9 +939,13 @@ class AwsProvider(Provider):
|
||||
|
||||
Returns:
|
||||
- A set of strings representing the available regions for the given service and partition.
|
||||
A service or a partition not present in the regions file yields an empty set, the same
|
||||
outcome as a service explicitly recorded as unavailable in the partition.
|
||||
"""
|
||||
data = read_aws_regions_file()
|
||||
json_regions = set(data["services"][service]["regions"][partition])
|
||||
json_regions = set(
|
||||
data["services"].get(service, {}).get("regions", {}).get(partition, [])
|
||||
)
|
||||
if audited_regions:
|
||||
# Get common regions between input and json
|
||||
regions = json_regions.intersection(audited_regions)
|
||||
@@ -1132,16 +1152,14 @@ class AwsProvider(Provider):
|
||||
Example:
|
||||
global_region = get_global_region()a
|
||||
"""
|
||||
global_region = "us-east-1"
|
||||
if self._identity.partition == "aws-cn":
|
||||
global_region = "cn-north-1"
|
||||
elif self._identity.partition == "aws-eusc":
|
||||
global_region = "eusc-de-east-1"
|
||||
elif self._identity.partition == "aws-us-gov":
|
||||
global_region = "us-gov-east-1"
|
||||
elif "aws-iso" in self._identity.partition:
|
||||
global_region = "aws-iso-global"
|
||||
return global_region
|
||||
# The first region of the partition is the one of its global STS endpoint,
|
||||
# which is always a real region, never a pseudo endpoint like "aws-iso-global"
|
||||
partition_regions = get_botocore_partition_regions().get(
|
||||
self._identity.partition
|
||||
)
|
||||
if partition_regions:
|
||||
return partition_regions[0]
|
||||
return "us-east-1"
|
||||
|
||||
@staticmethod
|
||||
def input_role_mfa_token_and_code() -> AWSMFAInfo:
|
||||
@@ -1158,26 +1176,35 @@ class AwsProvider(Provider):
|
||||
return AWSMFAInfo(arn=mfa_ARN, totp=mfa_TOTP)
|
||||
|
||||
@staticmethod
|
||||
def set_session_config(retries_max_attempts: int) -> Config:
|
||||
def set_session_config(
|
||||
retries_max_attempts: int,
|
||||
connect_timeout: Optional[int] = None,
|
||||
read_timeout: Optional[int] = None,
|
||||
) -> Config:
|
||||
"""
|
||||
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier configuration if nothing is passed as argument
|
||||
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier and timeout configuration if nothing is passed as argument
|
||||
|
||||
Args:
|
||||
- retries_max_attempts: The maximum number of retries for the standard retrier config
|
||||
- connect_timeout: Seconds to wait to establish a connection to an AWS endpoint
|
||||
- read_timeout: Seconds to wait for a response from an AWS endpoint
|
||||
|
||||
Returns:
|
||||
- Config: The botocore Config object
|
||||
"""
|
||||
default_session_config = get_default_session_config()
|
||||
if retries_max_attempts:
|
||||
default_session_config = default_session_config.merge(
|
||||
Config(
|
||||
retries={
|
||||
"max_attempts": retries_max_attempts,
|
||||
"mode": "standard",
|
||||
},
|
||||
)
|
||||
)
|
||||
overrides = {}
|
||||
if retries_max_attempts is not None:
|
||||
overrides["retries"] = {
|
||||
"max_attempts": retries_max_attempts,
|
||||
"mode": "standard",
|
||||
}
|
||||
if connect_timeout:
|
||||
overrides["connect_timeout"] = connect_timeout
|
||||
if read_timeout:
|
||||
overrides["read_timeout"] = read_timeout
|
||||
if overrides:
|
||||
default_session_config = default_session_config.merge(Config(**overrides))
|
||||
|
||||
return default_session_config
|
||||
|
||||
@@ -1420,12 +1447,6 @@ class AwsProvider(Provider):
|
||||
Connection(is_connected=True, Error=None))
|
||||
"""
|
||||
try:
|
||||
if aws_region is None:
|
||||
aws_region = (
|
||||
get_env_partition_bootstrap_region()
|
||||
or AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
)
|
||||
|
||||
session = AwsProvider.setup_session(
|
||||
mfa=mfa_enabled,
|
||||
profile=profile,
|
||||
@@ -1434,6 +1455,12 @@ class AwsProvider(Provider):
|
||||
aws_session_token=aws_session_token,
|
||||
)
|
||||
|
||||
if aws_region is None:
|
||||
aws_region = (
|
||||
get_env_partition_bootstrap_region(session.region_name)
|
||||
or AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
)
|
||||
|
||||
if role_arn:
|
||||
session_duration = validate_session_duration(session_duration)
|
||||
role_session_name = validate_role_session_name(role_session_name)
|
||||
@@ -1759,11 +1786,18 @@ def get_botocore_partition_regions() -> dict:
|
||||
return partition_regions
|
||||
|
||||
|
||||
def get_env_partition_regions() -> Optional[list]:
|
||||
def get_env_partition_regions(
|
||||
session_region: Optional[str] = None,
|
||||
) -> Optional[list]:
|
||||
"""
|
||||
Get the bootstrap region candidates for the partition set in the
|
||||
PROWLER_AWS_PARTITION environment variable.
|
||||
|
||||
Args:
|
||||
session_region (Optional[str]): The region of the AWS session. It leads
|
||||
the candidates when it belongs to the partition and is ignored
|
||||
otherwise.
|
||||
|
||||
Returns:
|
||||
Optional[list]: The regions of the configured partition, preferred
|
||||
bootstrap region first, or None when the environment variable is
|
||||
@@ -1782,14 +1816,25 @@ def get_env_partition_regions() -> Optional[list]:
|
||||
raise AWSInvalidPartitionError(
|
||||
message=f"Invalid partition: {raw_partition} set in PROWLER_AWS_PARTITION. Valid partitions: {', '.join(sorted(partition_regions))}"
|
||||
)
|
||||
|
||||
# A deployment reached only through its own region's endpoints has no route
|
||||
# to the partition's global STS region, so the session region goes first
|
||||
if session_region in regions:
|
||||
regions = [session_region] + [r for r in regions if r != session_region]
|
||||
return regions
|
||||
|
||||
|
||||
def get_env_partition_bootstrap_region() -> Optional[str]:
|
||||
def get_env_partition_bootstrap_region(
|
||||
session_region: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get the STS bootstrap region for the partition set in the
|
||||
PROWLER_AWS_PARTITION environment variable.
|
||||
|
||||
Args:
|
||||
session_region (Optional[str]): The region of the AWS session, preferred
|
||||
when it belongs to the partition.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The preferred bootstrap region of the configured
|
||||
partition, or None when the environment variable is not set.
|
||||
@@ -1797,7 +1842,7 @@ def get_env_partition_bootstrap_region() -> Optional[str]:
|
||||
Raises:
|
||||
AWSInvalidPartitionError: If the value is not a partition known to botocore.
|
||||
"""
|
||||
regions = get_env_partition_regions()
|
||||
regions = get_env_partition_regions(session_region)
|
||||
return regions[0] if regions else None
|
||||
|
||||
|
||||
@@ -1833,7 +1878,7 @@ def get_aws_region_for_sts(
|
||||
if region not in excluded_regions:
|
||||
return region
|
||||
|
||||
env_partition_regions = get_env_partition_regions()
|
||||
env_partition_regions = get_env_partition_regions(session_region)
|
||||
if env_partition_regions:
|
||||
# The configured partition constrains the whole fallback chain: prefer
|
||||
# a non-excluded region, but never leave the partition
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,39 @@ import os
|
||||
|
||||
from botocore.config import Config
|
||||
|
||||
from prowler.providers.aws.exceptions.exceptions import AWSInvalidBoto3TimeoutError
|
||||
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION = "us-east-1"
|
||||
AWS_REGION_US_EAST_1 = "us-east-1"
|
||||
BOTO3_USER_AGENT_EXTRA = os.getenv("PROWLER_AWS_BOTO3_USER_AGENT_EXTRA", "APN_1826889")
|
||||
BOTO3_RETRIES_MAX_ATTEMPTS = 3
|
||||
# botocore defaults both to 60s
|
||||
BOTO3_CONNECT_TIMEOUT = 10
|
||||
BOTO3_READ_TIMEOUT = 60
|
||||
ROLE_SESSION_NAME = "ProwlerAssessmentSession"
|
||||
|
||||
|
||||
def get_boto3_timeout_from_env(name: str, default: int) -> int:
|
||||
"""Positive integer seconds read from the environment, or default when unset."""
|
||||
raw = os.getenv(name, "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
if not raw.isdecimal() or int(raw) == 0:
|
||||
raise AWSInvalidBoto3TimeoutError(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"{name} must be a positive integer number of seconds, got {raw!r}",
|
||||
)
|
||||
return int(raw)
|
||||
|
||||
|
||||
def get_default_session_config() -> Config:
|
||||
return Config(
|
||||
user_agent_extra=BOTO3_USER_AGENT_EXTRA,
|
||||
retries={"max_attempts": 3, "mode": "standard"},
|
||||
retries={"max_attempts": BOTO3_RETRIES_MAX_ATTEMPTS, "mode": "standard"},
|
||||
connect_timeout=get_boto3_timeout_from_env(
|
||||
"PROWLER_AWS_BOTO3_CONNECT_TIMEOUT", BOTO3_CONNECT_TIMEOUT
|
||||
),
|
||||
read_timeout=get_boto3_timeout_from_env(
|
||||
"PROWLER_AWS_BOTO3_READ_TIMEOUT", BOTO3_READ_TIMEOUT
|
||||
),
|
||||
)
|
||||
|
||||
@@ -78,6 +78,10 @@ class AWSBaseException(ProwlerException):
|
||||
"message": "The provided AWS partition is invalid",
|
||||
"remediation": "Check the provided AWS partition and ensure it is valid.",
|
||||
},
|
||||
(1918, "AWSInvalidBoto3TimeoutError"): {
|
||||
"message": "The Boto3 timeout configured through the environment is invalid",
|
||||
"remediation": "Set PROWLER_AWS_BOTO3_CONNECT_TIMEOUT and PROWLER_AWS_BOTO3_READ_TIMEOUT to a positive integer number of seconds.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
@@ -231,3 +235,12 @@ class AWSInvalidPartitionError(AWSBaseException):
|
||||
super().__init__(
|
||||
1917, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSInvalidBoto3TimeoutError(AWSBaseException):
|
||||
"""Boto3 timeout configured through the environment is not a positive integer."""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1918, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
@@ -156,7 +156,21 @@ def init_parser(self):
|
||||
nargs="?",
|
||||
default=None,
|
||||
type=int,
|
||||
help="Set the maximum attemps for the Boto3 standard retrier config (Default: 3)",
|
||||
help="Set the maximum retries for the Boto3 standard retrier config, 0 disables retries (Default: 3)",
|
||||
)
|
||||
boto3_config_subparser.add_argument(
|
||||
"--aws-connect-timeout",
|
||||
nargs="?",
|
||||
default=None,
|
||||
type=validate_timeout,
|
||||
help="Seconds to wait to establish a connection (TCP, proxy tunnel and TLS) to an AWS endpoint before retrying (Default: 10)",
|
||||
)
|
||||
boto3_config_subparser.add_argument(
|
||||
"--aws-read-timeout",
|
||||
nargs="?",
|
||||
default=None,
|
||||
type=validate_timeout,
|
||||
help="Seconds to wait for a response from an AWS endpoint before retrying (Default: 60)",
|
||||
)
|
||||
|
||||
# Scan Unused Services
|
||||
@@ -190,6 +204,13 @@ def validate_session_duration(session_duration: int) -> int:
|
||||
return duration
|
||||
|
||||
|
||||
def validate_timeout(value: str) -> int:
|
||||
"""validate_timeout validates that the input is a whole number of seconds greater than zero"""
|
||||
if not value.isdecimal() or int(value) == 0:
|
||||
raise ArgumentTypeError(f"{value} is not a positive integer")
|
||||
return int(value)
|
||||
|
||||
|
||||
def validate_role_session_name(session_name) -> str:
|
||||
"""
|
||||
Validates that the role session name is valid.
|
||||
|
||||
@@ -42,6 +42,8 @@ class AwsSetUpSession:
|
||||
aws_session_token: Optional[str] = None,
|
||||
retries_max_attempts: int = 3,
|
||||
regions: set = set(),
|
||||
connect_timeout: Optional[int] = None,
|
||||
read_timeout: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
The constructor for the AwsSetUpSession class.
|
||||
@@ -58,6 +60,8 @@ class AwsSetUpSession:
|
||||
- aws_session_token: The AWS session token, optional.
|
||||
- retries_max_attempts: The maximum number of retries for the AWS client.
|
||||
- regions: A set of regions to audit.
|
||||
- connect_timeout: Seconds to wait to establish a connection to an AWS endpoint.
|
||||
- read_timeout: Seconds to wait for a response from an AWS endpoint.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -73,7 +77,9 @@ class AwsSetUpSession:
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
)
|
||||
session_config = AwsProvider.set_session_config(retries_max_attempts)
|
||||
session_config = AwsProvider.set_session_config(
|
||||
retries_max_attempts, connect_timeout, read_timeout
|
||||
)
|
||||
aws_session = AwsProvider.setup_session(
|
||||
mfa=mfa,
|
||||
profile=profile,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ class codebuild_project_uses_allowed_github_organizations(Check):
|
||||
project_role = next(
|
||||
(
|
||||
role
|
||||
for role in iam_client.roles
|
||||
for role in iam_client.roles or []
|
||||
if role.arn == project.service_role_arn
|
||||
),
|
||||
None,
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ class iam_role_service_trust_restricts_source_to_account(Check):
|
||||
status. The sibling token-wildcard check carries the same note, for the same reason.
|
||||
"""
|
||||
findings = []
|
||||
for role in iam_client.roles:
|
||||
for role in iam_client.roles or []:
|
||||
# Service-linked roles are excluded: their trust relationship is managed by
|
||||
# the service and cannot be edited, so a finding would not be actionable.
|
||||
if "aws-service-role" in role.arn:
|
||||
|
||||
+3
-1
@@ -205,7 +205,9 @@ class rolesanywhere_profile_restricts_session_permissions(Check):
|
||||
not administrative, and disabled profiles.
|
||||
"""
|
||||
findings = []
|
||||
roles_by_arn = {role.arn: role for role in iam_client.roles}
|
||||
# iam:ListRoles denied leaves roles as None: every referenced role is
|
||||
# then unknown and the profile falls through to MANUAL.
|
||||
roles_by_arn = {role.arn: role for role in (iam_client.roles or [])}
|
||||
for profile in rolesanywhere_client.profiles.values():
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=profile)
|
||||
role_statuses = {
|
||||
|
||||
@@ -382,6 +382,8 @@ class Provider(ABC):
|
||||
)
|
||||
provider_class(
|
||||
retries_max_attempts=arguments.aws_retries_max_attempts,
|
||||
connect_timeout=arguments.aws_connect_timeout,
|
||||
read_timeout=arguments.aws_read_timeout,
|
||||
role_arn=arguments.role,
|
||||
session_duration=arguments.session_duration,
|
||||
external_id=arguments.external_id,
|
||||
|
||||
@@ -115,10 +115,15 @@ class ImageProvider(Provider):
|
||||
self._session = None
|
||||
self._identity = "prowler"
|
||||
self._listing_only = False
|
||||
self._trivy_cache_dir_obj = tempfile.TemporaryDirectory(
|
||||
prefix="prowler-trivy-cache-"
|
||||
)
|
||||
self._trivy_cache_dir = self._trivy_cache_dir_obj.name
|
||||
# A supplied cache dir is never deleted: it may hold a DB we cannot refetch
|
||||
configured_cache_dir = os.environ.get("TRIVY_CACHE_DIR", "").strip()
|
||||
if configured_cache_dir:
|
||||
self._trivy_cache_dir = configured_cache_dir
|
||||
else:
|
||||
self._trivy_cache_dir_obj = tempfile.TemporaryDirectory(
|
||||
prefix="prowler-trivy-cache-"
|
||||
)
|
||||
self._trivy_cache_dir = self._trivy_cache_dir_obj.name
|
||||
|
||||
# Registry authentication (follows IaC pattern: explicit params, env vars internal)
|
||||
self.registry_username = registry_username or os.environ.get(
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
version = "5.42.0"
|
||||
version = "5.43.0"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -1152,6 +1152,35 @@ class Test_Parser:
|
||||
parsed = self.parser.parse(command)
|
||||
assert parsed.aws_retries_max_attempts == int(max_retries)
|
||||
|
||||
def test_aws_parser_retries_max_attempts_zero(self):
|
||||
command = [prowler_command, "--aws-retries-max-attempts", "0"]
|
||||
parsed = self.parser.parse(command)
|
||||
assert parsed.aws_retries_max_attempts == 0
|
||||
|
||||
def test_aws_parser_timeouts_default_to_none(self):
|
||||
parsed = self.parser.parse([prowler_command])
|
||||
assert parsed.aws_connect_timeout is None
|
||||
assert parsed.aws_read_timeout is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"argument, attribute",
|
||||
[
|
||||
("--aws-connect-timeout", "aws_connect_timeout"),
|
||||
("--aws-read-timeout", "aws_read_timeout"),
|
||||
],
|
||||
)
|
||||
def test_aws_parser_timeouts(self, argument, attribute):
|
||||
timeout = "5"
|
||||
command = [prowler_command, argument, timeout]
|
||||
parsed = self.parser.parse(command)
|
||||
assert getattr(parsed, attribute) == int(timeout)
|
||||
|
||||
@pytest.mark.parametrize("value", ["0", "-1", "abc"])
|
||||
def test_aws_parser_connect_timeout_rejects_non_positive(self, value):
|
||||
command = [prowler_command, "--aws-connect-timeout", value]
|
||||
with pytest.raises(SystemExit):
|
||||
self.parser.parse(command)
|
||||
|
||||
def test_aws_parser_scan_unused_services(self):
|
||||
argument = "--scan-unused-services"
|
||||
command = [prowler_command, argument]
|
||||
|
||||
@@ -16,22 +16,32 @@ from moto import mock_aws
|
||||
from pytest import raises
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from prowler.providers.aws.aws_provider import AwsProvider, get_aws_region_for_sts
|
||||
from prowler.providers.aws.aws_provider import (
|
||||
AwsProvider,
|
||||
get_aws_region_for_sts,
|
||||
get_env_partition_bootstrap_region,
|
||||
get_env_partition_regions,
|
||||
)
|
||||
from prowler.providers.aws.config import (
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
BOTO3_CONNECT_TIMEOUT,
|
||||
BOTO3_READ_TIMEOUT,
|
||||
BOTO3_USER_AGENT_EXTRA,
|
||||
ROLE_SESSION_NAME,
|
||||
get_boto3_timeout_from_env,
|
||||
get_default_session_config,
|
||||
)
|
||||
from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSArgumentTypeValidationError,
|
||||
AWSIAMRoleARNInvalidResourceTypeError,
|
||||
AWSInvalidBoto3TimeoutError,
|
||||
AWSInvalidPartitionError,
|
||||
AWSInvalidProviderIdError,
|
||||
AWSNoCredentialsError,
|
||||
)
|
||||
from prowler.providers.aws.lib.arn.models import ARN
|
||||
from prowler.providers.aws.lib.mutelist.mutelist import AWSMutelist
|
||||
from prowler.providers.aws.lib.session.aws_set_up_session import AwsSetUpSession
|
||||
from prowler.providers.aws.models import (
|
||||
AWSAssumeRoleInfo,
|
||||
AWSCallerIdentity,
|
||||
@@ -49,6 +59,7 @@ from tests.providers.aws.utils import (
|
||||
AWS_EUSC_PARTITION,
|
||||
AWS_GOV_CLOUD_ACCOUNT_ARN,
|
||||
AWS_GOV_CLOUD_PARTITION,
|
||||
AWS_ISO_B_PARTITION,
|
||||
AWS_ISO_PARTITION,
|
||||
AWS_REGION_CN_NORTH_1,
|
||||
AWS_REGION_CN_NORTHWEST_1,
|
||||
@@ -56,7 +67,10 @@ from tests.providers.aws.utils import (
|
||||
AWS_REGION_EU_WEST_1,
|
||||
AWS_REGION_EUSC_DE_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_ISO_GLOBAL,
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
AWS_REGION_ISO_B_EAST_1,
|
||||
AWS_REGION_ISO_EAST_1,
|
||||
AWS_REGION_ISO_WEST_1,
|
||||
AWS_REGION_US_EAST_1,
|
||||
AWS_REGION_US_EAST_2,
|
||||
EXAMPLE_AMI_ID,
|
||||
@@ -1181,6 +1195,13 @@ aws:
|
||||
== AWS_REGION_EU_WEST_1
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_aws_get_global_region(self):
|
||||
aws_provider = AwsProvider()
|
||||
aws_provider._identity.partition = AWS_COMMERCIAL_PARTITION
|
||||
|
||||
assert aws_provider.get_global_region() == AWS_REGION_US_EAST_1
|
||||
|
||||
@mock_aws
|
||||
def test_aws_gov_get_global_region(self):
|
||||
aws_provider = AwsProvider()
|
||||
@@ -1200,7 +1221,21 @@ aws:
|
||||
aws_provider = AwsProvider()
|
||||
aws_provider._identity.partition = AWS_ISO_PARTITION
|
||||
|
||||
assert aws_provider.get_global_region() == AWS_REGION_ISO_GLOBAL
|
||||
assert aws_provider.get_global_region() == AWS_REGION_ISO_EAST_1
|
||||
|
||||
@mock_aws
|
||||
def test_aws_iso_b_get_global_region(self):
|
||||
aws_provider = AwsProvider()
|
||||
aws_provider._identity.partition = AWS_ISO_B_PARTITION
|
||||
|
||||
assert aws_provider.get_global_region() == AWS_REGION_ISO_B_EAST_1
|
||||
|
||||
@mock_aws
|
||||
def test_get_global_region_for_an_unknown_partition(self):
|
||||
aws_provider = AwsProvider()
|
||||
aws_provider._identity.partition = "aws-unknown"
|
||||
|
||||
assert aws_provider.get_global_region() == AWS_REGION_US_EAST_1
|
||||
|
||||
@mock_aws
|
||||
def test_aws_eusc_get_global_region(self):
|
||||
@@ -1288,6 +1323,88 @@ aws:
|
||||
len(aws_provider.get_available_aws_service_regions("ec2", "aws")) == 17
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_get_available_aws_service_regions_commercial_and_gov_cloud(self):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
assert AWS_REGION_US_EAST_1 in aws_provider.get_available_aws_service_regions(
|
||||
"ec2", AWS_COMMERCIAL_PARTITION
|
||||
)
|
||||
assert (
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
in aws_provider.get_available_aws_service_regions(
|
||||
"ec2", AWS_GOV_CLOUD_PARTITION
|
||||
)
|
||||
)
|
||||
# A service recorded as unavailable in the partition yields an empty set
|
||||
assert (
|
||||
aws_provider.get_available_aws_service_regions(
|
||||
"bedrock-agent", AWS_CHINA_PARTITION
|
||||
)
|
||||
== set()
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_get_available_aws_service_regions_iso_partitions(self):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
assert aws_provider.get_available_aws_service_regions(
|
||||
"ec2", AWS_ISO_PARTITION
|
||||
) == {
|
||||
AWS_REGION_ISO_EAST_1,
|
||||
AWS_REGION_ISO_WEST_1,
|
||||
}
|
||||
assert aws_provider.get_available_aws_service_regions(
|
||||
"guardduty", AWS_ISO_B_PARTITION
|
||||
) == {AWS_REGION_ISO_B_EAST_1}
|
||||
# Every service carries every ISO partition, empty when not available
|
||||
assert (
|
||||
aws_provider.get_available_aws_service_regions(
|
||||
"bedrock", AWS_ISO_B_PARTITION
|
||||
)
|
||||
== set()
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_get_available_aws_service_regions_unknown_partition(self):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
assert (
|
||||
aws_provider.get_available_aws_service_regions("ec2", "aws-unknown")
|
||||
== set()
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_get_available_aws_service_regions_unknown_service(self):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
assert (
|
||||
aws_provider.get_available_aws_service_regions(
|
||||
"unknown-service", AWS_COMMERCIAL_PARTITION
|
||||
)
|
||||
== set()
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_generate_regional_clients_service_not_in_partition(self):
|
||||
aws_provider = AwsProvider()
|
||||
aws_provider._identity.partition = AWS_ISO_PARTITION
|
||||
|
||||
response = aws_provider.generate_regional_clients("bedrock")
|
||||
|
||||
assert response == {}
|
||||
|
||||
@mock_aws
|
||||
def test_generate_regional_clients_returns_empty_dict_on_error(self):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
with patch.object(
|
||||
AwsProvider,
|
||||
"get_available_aws_service_regions",
|
||||
side_effect=Exception("boom"),
|
||||
):
|
||||
assert aws_provider.generate_regional_clients("ec2") == {}
|
||||
|
||||
@mock_aws
|
||||
def test_get_tagged_resources(self):
|
||||
ec2_client = client("ec2", region_name=AWS_REGION_EU_CENTRAL_1)
|
||||
@@ -2054,7 +2171,8 @@ aws:
|
||||
assert not recovered_regions
|
||||
|
||||
def test_get_regions_all_count(self):
|
||||
assert len(AwsProvider.get_regions(partition=None)) == 39
|
||||
# 34 aws + 2 aws-cn + 2 aws-us-gov + 1 aws-eusc + 7 ISO regions
|
||||
assert len(AwsProvider.get_regions(partition=None)) == 46
|
||||
|
||||
def test_get_regions_cn_count(self):
|
||||
assert len(AwsProvider.get_regions("aws-cn")) == 2
|
||||
@@ -2062,6 +2180,12 @@ aws:
|
||||
def test_get_regions_aws_count(self):
|
||||
assert len(AwsProvider.get_regions(partition="aws")) == 34
|
||||
|
||||
def test_get_regions_iso_count(self):
|
||||
assert AwsProvider.get_regions(AWS_ISO_PARTITION) == {
|
||||
AWS_REGION_ISO_EAST_1,
|
||||
AWS_REGION_ISO_WEST_1,
|
||||
}
|
||||
|
||||
def test_get_all_regions(self):
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.read_aws_regions_file",
|
||||
@@ -2447,6 +2571,245 @@ aws:
|
||||
== AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
def test_get_env_partition_regions_leads_with_session_region(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
regions = get_env_partition_regions(AWS_REGION_GOV_CLOUD_US_WEST_1)
|
||||
|
||||
assert regions[0] == AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
assert set(regions) == set(get_env_partition_regions())
|
||||
|
||||
def test_get_env_partition_regions_ignores_session_region_outside_partition(
|
||||
self,
|
||||
):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
regions = get_env_partition_regions(AWS_REGION_EU_WEST_1)
|
||||
|
||||
assert regions[0] == AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
assert AWS_REGION_EU_WEST_1 not in regions
|
||||
|
||||
def test_get_env_partition_bootstrap_region_prefers_session_region(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
assert (
|
||||
get_env_partition_bootstrap_region(AWS_REGION_GOV_CLOUD_US_WEST_1)
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
def test_get_env_partition_bootstrap_region_without_session_region(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
assert (
|
||||
get_env_partition_bootstrap_region() == AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
def test_get_env_partition_bootstrap_region_without_partition(self):
|
||||
with mock.patch.dict(os.environ, {"PROWLER_AWS_PARTITION": ""}, clear=False):
|
||||
assert (
|
||||
get_env_partition_bootstrap_region(AWS_REGION_GOV_CLOUD_US_WEST_1)
|
||||
is None
|
||||
)
|
||||
|
||||
def test_get_aws_region_for_sts_env_partition_prefers_session_region(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
assert (
|
||||
get_aws_region_for_sts(AWS_REGION_GOV_CLOUD_US_WEST_1, None)
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
def test_get_profile_region_env_partition_keeps_session_region_inside_partition(
|
||||
self,
|
||||
):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
aws_session = session.Session(region_name=AWS_REGION_GOV_CLOUD_US_WEST_1)
|
||||
|
||||
assert (
|
||||
AwsProvider.get_profile_region(aws_session)
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
def test_get_profile_region_env_partition_ignores_session_region_outside_partition(
|
||||
self,
|
||||
):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
aws_session = session.Session(region_name=AWS_REGION_US_EAST_1)
|
||||
|
||||
assert (
|
||||
AwsProvider.get_profile_region(aws_session)
|
||||
== AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
def test_get_profile_region_env_partition_excluded_session_region_stays_in_partition(
|
||||
self,
|
||||
):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
aws_session = session.Session(region_name=AWS_REGION_GOV_CLOUD_US_WEST_1)
|
||||
|
||||
assert (
|
||||
AwsProvider.get_profile_region(
|
||||
aws_session, {AWS_REGION_GOV_CLOUD_US_WEST_1}
|
||||
)
|
||||
== AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
def test_get_profile_region_env_partition_all_regions_excluded_stays_in_partition(
|
||||
self,
|
||||
):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION},
|
||||
clear=False,
|
||||
):
|
||||
aws_session = session.Session(region_name=AWS_REGION_GOV_CLOUD_US_WEST_1)
|
||||
gov_cloud_regions = set(get_env_partition_regions())
|
||||
|
||||
assert (
|
||||
AwsProvider.get_profile_region(aws_session, gov_cloud_regions)
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_test_connection_env_partition_prefers_session_region(self):
|
||||
with (
|
||||
mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION,
|
||||
"AWS_DEFAULT_REGION": AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
},
|
||||
clear=False,
|
||||
),
|
||||
mock.patch.object(
|
||||
AwsProvider,
|
||||
"validate_credentials",
|
||||
return_value=AWSCallerIdentity(
|
||||
user_id="test-user-id",
|
||||
account=AWS_ACCOUNT_NUMBER,
|
||||
arn=ARN(AWS_GOV_CLOUD_ACCOUNT_ARN),
|
||||
region=AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
),
|
||||
) as mock_validate_credentials,
|
||||
):
|
||||
connection = AwsProvider.test_connection(
|
||||
aws_access_key_id="test-access-key",
|
||||
aws_secret_access_key="test-secret-key",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection.is_connected
|
||||
assert (
|
||||
mock_validate_credentials.call_args.args[1]
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_test_connection_role_env_partition_prefers_session_region(self):
|
||||
with (
|
||||
mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION,
|
||||
"AWS_DEFAULT_REGION": AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
},
|
||||
clear=False,
|
||||
),
|
||||
mock.patch.object(
|
||||
AwsProvider,
|
||||
"assume_role",
|
||||
return_value=AWSCredentials(
|
||||
aws_access_key_id="assumed-access-key",
|
||||
aws_secret_access_key="assumed-secret-key",
|
||||
aws_session_token="assumed-session-token",
|
||||
expiration=datetime.now(),
|
||||
),
|
||||
) as mock_assume_role,
|
||||
mock.patch.object(
|
||||
AwsProvider,
|
||||
"validate_credentials",
|
||||
return_value=AWSCallerIdentity(
|
||||
user_id="test-user-id",
|
||||
account=AWS_ACCOUNT_NUMBER,
|
||||
arn=ARN(AWS_GOV_CLOUD_ACCOUNT_ARN),
|
||||
region=AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
),
|
||||
),
|
||||
):
|
||||
connection = AwsProvider.test_connection(
|
||||
role_arn=f"arn:{AWS_GOV_CLOUD_PARTITION}:iam::{AWS_ACCOUNT_NUMBER}:role/test-role",
|
||||
aws_access_key_id="test-access-key",
|
||||
aws_secret_access_key="test-secret-key",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection.is_connected
|
||||
assumed_role_info = mock_assume_role.call_args.args[1]
|
||||
assert assumed_role_info.sts_region == AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
|
||||
@mock_aws
|
||||
def test_setup_session_mfa_env_partition_prefers_session_region(self):
|
||||
with (
|
||||
mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PROWLER_AWS_PARTITION": AWS_GOV_CLOUD_PARTITION,
|
||||
"AWS_DEFAULT_REGION": AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
},
|
||||
clear=False,
|
||||
),
|
||||
mock.patch.object(
|
||||
AwsProvider,
|
||||
"input_role_mfa_token_and_code",
|
||||
return_value=AWSMFAInfo(
|
||||
arn=f"arn:{AWS_GOV_CLOUD_PARTITION}:iam::{AWS_ACCOUNT_NUMBER}:mfa/test",
|
||||
totp="123456",
|
||||
),
|
||||
),
|
||||
mock.patch.object(
|
||||
AwsProvider,
|
||||
"create_sts_session",
|
||||
side_effect=AwsProvider.create_sts_session,
|
||||
) as mock_create_sts_session,
|
||||
):
|
||||
AwsProvider.setup_session(
|
||||
mfa=True,
|
||||
aws_access_key_id="test-access-key",
|
||||
aws_secret_access_key="test-secret-key",
|
||||
)
|
||||
|
||||
assert (
|
||||
mock_create_sts_session.call_args.args[1]
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_test_connection_env_partition_mismatch(self):
|
||||
with (
|
||||
@@ -2490,6 +2853,8 @@ aws:
|
||||
|
||||
assert session_config.user_agent_extra == BOTO3_USER_AGENT_EXTRA
|
||||
assert session_config.retries == {"max_attempts": 3, "mode": "standard"}
|
||||
assert session_config.connect_timeout == BOTO3_CONNECT_TIMEOUT
|
||||
assert session_config.read_timeout == BOTO3_READ_TIMEOUT
|
||||
|
||||
@mock_aws
|
||||
def test_set_session_config_10_max_attempts(self):
|
||||
@@ -2498,12 +2863,93 @@ aws:
|
||||
|
||||
assert session_config.user_agent_extra == BOTO3_USER_AGENT_EXTRA
|
||||
assert session_config.retries == {"max_attempts": 10, "mode": "standard"}
|
||||
assert session_config.connect_timeout == BOTO3_CONNECT_TIMEOUT
|
||||
assert session_config.read_timeout == BOTO3_READ_TIMEOUT
|
||||
|
||||
def test_set_session_config_0_max_attempts_disables_retries(self):
|
||||
session_config = AwsProvider.set_session_config(0)
|
||||
|
||||
assert session_config.retries == {"max_attempts": 0, "mode": "standard"}
|
||||
|
||||
@mock_aws
|
||||
def test_aws_provider_0_max_attempts_reaches_clients(self):
|
||||
aws_provider = AwsProvider(retries_max_attempts=0)
|
||||
client = aws_provider.session.current_session.client(
|
||||
"ec2", region_name=AWS_REGION_US_EAST_1
|
||||
)
|
||||
|
||||
# botocore rewrites max_attempts into total_max_attempts (retries + 1)
|
||||
assert client.meta.config.retries["total_max_attempts"] == 1
|
||||
|
||||
def test_set_session_config_timeouts(self):
|
||||
session_config = AwsProvider.set_session_config(
|
||||
None, connect_timeout=2, read_timeout=15
|
||||
)
|
||||
|
||||
assert session_config.retries == {"max_attempts": 3, "mode": "standard"}
|
||||
assert session_config.connect_timeout == 2
|
||||
assert session_config.read_timeout == 15
|
||||
|
||||
@mock_aws
|
||||
def test_aws_provider_timeouts_reach_session_config(self):
|
||||
aws_provider = AwsProvider(connect_timeout=2, read_timeout=15)
|
||||
|
||||
assert aws_provider.session.session_config.connect_timeout == 2
|
||||
assert aws_provider.session.session_config.read_timeout == 15
|
||||
|
||||
@mock_aws
|
||||
def test_aws_set_up_session_forwards_timeouts(self):
|
||||
aws_session = AwsSetUpSession(
|
||||
aws_access_key_id="testing",
|
||||
aws_secret_access_key="testing",
|
||||
connect_timeout=2,
|
||||
read_timeout=15,
|
||||
)
|
||||
|
||||
assert aws_session._session.session_config.connect_timeout == 2
|
||||
assert aws_session._session.session_config.read_timeout == 15
|
||||
|
||||
def test_get_default_session_config(self):
|
||||
config = get_default_session_config()
|
||||
|
||||
assert config.user_agent_extra == BOTO3_USER_AGENT_EXTRA
|
||||
assert config.retries == {"max_attempts": 3, "mode": "standard"}
|
||||
assert config.connect_timeout == BOTO3_CONNECT_TIMEOUT
|
||||
assert config.read_timeout == BOTO3_READ_TIMEOUT
|
||||
|
||||
def test_get_default_session_config_timeouts_from_env(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PROWLER_AWS_BOTO3_CONNECT_TIMEOUT": "3",
|
||||
"PROWLER_AWS_BOTO3_READ_TIMEOUT": "20",
|
||||
},
|
||||
):
|
||||
config = get_default_session_config()
|
||||
|
||||
assert config.connect_timeout == 3
|
||||
assert config.read_timeout == 20
|
||||
|
||||
def test_set_session_config_argument_overrides_env_timeouts(self):
|
||||
with mock.patch.dict(os.environ, {"PROWLER_AWS_BOTO3_CONNECT_TIMEOUT": "3"}):
|
||||
config = AwsProvider.set_session_config(None, connect_timeout=7)
|
||||
|
||||
assert config.connect_timeout == 7
|
||||
|
||||
@pytest.mark.parametrize("raw", ["0", "-5", "ten", "1.5"])
|
||||
def test_get_boto3_timeout_from_env_rejects_non_positive_integers(self, raw):
|
||||
with mock.patch.dict(os.environ, {"PROWLER_AWS_BOTO3_CONNECT_TIMEOUT": raw}):
|
||||
with raises(
|
||||
AWSInvalidBoto3TimeoutError, match="PROWLER_AWS_BOTO3_CONNECT_TIMEOUT"
|
||||
):
|
||||
get_boto3_timeout_from_env("PROWLER_AWS_BOTO3_CONNECT_TIMEOUT", 10)
|
||||
|
||||
def test_get_boto3_timeout_from_env_blank_falls_back_to_default(self):
|
||||
with mock.patch.dict(os.environ, {"PROWLER_AWS_BOTO3_CONNECT_TIMEOUT": " "}):
|
||||
assert (
|
||||
get_boto3_timeout_from_env("PROWLER_AWS_BOTO3_CONNECT_TIMEOUT", 10)
|
||||
== 10
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
@patch(
|
||||
|
||||
+54
-1
@@ -1,4 +1,4 @@
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
@@ -182,6 +182,59 @@ class Test_codebuild_project_uses_allowed_github_organizations:
|
||||
)
|
||||
assert result[0].region == AWS_REGION_EU_WEST_1
|
||||
|
||||
@mock_aws
|
||||
def test_project_github_with_unlisted_roles(self):
|
||||
# iam:ListRoles denied leaves iam_client.roles as None.
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
codebuild_client = client("codebuild", region_name=AWS_REGION_EU_WEST_1)
|
||||
codebuild_client.create_project(
|
||||
name="test-project-github-unlisted-roles",
|
||||
source={
|
||||
"type": "GITHUB",
|
||||
"location": "https://github.com/allowed-org/repo",
|
||||
},
|
||||
artifacts={"type": "NO_ARTIFACTS"},
|
||||
environment={
|
||||
"type": "LINUX_CONTAINER",
|
||||
"image": "aws/codebuild/standard:4.0",
|
||||
"computeType": "BUILD_GENERAL1_SMALL",
|
||||
"environmentVariables": [],
|
||||
},
|
||||
serviceRole=f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/codebuild-test-role",
|
||||
)
|
||||
|
||||
from prowler.providers.aws.services.codebuild.codebuild_service import Codebuild
|
||||
|
||||
iam_client = MagicMock()
|
||||
iam_client.roles = None
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_uses_allowed_github_organizations.codebuild_project_uses_allowed_github_organizations.codebuild_client",
|
||||
new=Codebuild(aws_provider),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_uses_allowed_github_organizations.codebuild_project_uses_allowed_github_organizations.iam_client",
|
||||
new=iam_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.codebuild.codebuild_project_uses_allowed_github_organizations.codebuild_project_uses_allowed_github_organizations.codebuild_client.audit_config",
|
||||
{"codebuild_github_allowed_organizations": ["allowed-org"]},
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.codebuild.codebuild_project_uses_allowed_github_organizations.codebuild_project_uses_allowed_github_organizations import (
|
||||
codebuild_project_uses_allowed_github_organizations,
|
||||
)
|
||||
|
||||
assert (
|
||||
len(codebuild_project_uses_allowed_github_organizations().execute())
|
||||
== 0
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_project_github_no_codebuild_trusted_principal(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
+4
@@ -93,6 +93,10 @@ class Test_iam_role_service_trust_restricts_source_to_account:
|
||||
"""An account with no roles produces no reports at all."""
|
||||
assert len(_run([])) == 0
|
||||
|
||||
def test_unlisted_roles_produce_no_reports(self):
|
||||
# iam:ListRoles denied leaves iam_client.roles as None.
|
||||
assert len(_run(None)) == 0
|
||||
|
||||
def test_service_linked_role_skipped(self):
|
||||
"""A service-linked role is excluded even when its trust policy would FAIL.
|
||||
|
||||
|
||||
+13
@@ -472,6 +472,19 @@ class Test_rolesanywhere_profile_restricts_session_permissions:
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "could not be evaluated" in result[0].status_extended
|
||||
|
||||
def test_unscoped_profile_with_unlisted_roles_is_manual(self):
|
||||
# iam:ListRoles denied leaves iam_client.roles as None.
|
||||
patches = _patched(
|
||||
_build_client({PROFILE_ARN: _profile(role_arns=[ADMIN_ROLE_ARN])})
|
||||
)
|
||||
patches[-1].new.roles = None
|
||||
with _enter(patches):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert ADMIN_ROLE_ARN in result[0].status_extended
|
||||
assert "could not be evaluated" in result[0].status_extended
|
||||
|
||||
def test_unscoped_profile_without_roles_passes(self):
|
||||
with _enter(_patched(_build_client({PROFILE_ARN: _profile(role_arns=[])}))):
|
||||
result = _run()
|
||||
|
||||
-6
@@ -103,12 +103,6 @@ class TestSecretsManagerHasRestrictiveResourcePolicy:
|
||||
with mock_aws():
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
|
||||
|
||||
from prowler.providers.aws.services.secretsmanager.secretsmanager_has_restrictive_resource_policy.secretsmanager_has_restrictive_resource_policy import (
|
||||
secretsmanager_client,
|
||||
)
|
||||
|
||||
secretsmanager_client.secrets.clear()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
|
||||
@@ -21,6 +21,7 @@ AWS_GOV_CLOUD_PARTITION = "aws-us-gov"
|
||||
AWS_CHINA_PARTITION = "aws-cn"
|
||||
AWS_EUSC_PARTITION = "aws-eusc"
|
||||
AWS_ISO_PARTITION = "aws-iso"
|
||||
AWS_ISO_B_PARTITION = "aws-iso-b"
|
||||
|
||||
# Root AWS Account
|
||||
AWS_ACCOUNT_NUMBER = "123456789012"
|
||||
@@ -51,9 +52,12 @@ AWS_REGION_CN_NORTH_1 = "cn-north-1"
|
||||
|
||||
# Gov Cloud Regions
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1 = "us-gov-east-1"
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1 = "us-gov-west-1"
|
||||
|
||||
# Iso Regions
|
||||
AWS_REGION_ISO_GLOBAL = "aws-iso-global"
|
||||
AWS_REGION_ISO_EAST_1 = "us-iso-east-1"
|
||||
AWS_REGION_ISO_WEST_1 = "us-iso-west-1"
|
||||
AWS_REGION_ISO_B_EAST_1 = "us-isob-east-1"
|
||||
|
||||
# European Sovereign Cloud Regions
|
||||
AWS_REGION_EUSC_DE_EAST_1 = "eusc-de-east-1"
|
||||
|
||||
@@ -50,6 +50,11 @@ def _make_provider(**kwargs):
|
||||
return ImageProvider(**defaults)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_configured_cache_dir(monkeypatch):
|
||||
monkeypatch.delenv("TRIVY_CACHE_DIR", raising=False)
|
||||
|
||||
|
||||
class TestImageProvider:
|
||||
def test_image_provider(self):
|
||||
"""Test default initialization."""
|
||||
@@ -999,6 +1004,34 @@ class TestCleanup:
|
||||
provider.cleanup()
|
||||
provider.cleanup()
|
||||
|
||||
def test_configured_cache_dir_is_used(self, monkeypatch, tmp_path):
|
||||
"""A deployment that supplies a cache directory gets that one."""
|
||||
monkeypatch.setenv("TRIVY_CACHE_DIR", str(tmp_path))
|
||||
|
||||
provider = _make_provider()
|
||||
|
||||
assert provider._trivy_cache_dir == str(tmp_path)
|
||||
|
||||
def test_configured_cache_dir_survives_cleanup(self, monkeypatch, tmp_path):
|
||||
"""A supplied directory is not the provider's to delete: it holds a
|
||||
database the deployment may have no way to fetch again."""
|
||||
monkeypatch.setenv("TRIVY_CACHE_DIR", str(tmp_path))
|
||||
provider = _make_provider()
|
||||
|
||||
provider.cleanup()
|
||||
|
||||
assert os.path.isdir(str(tmp_path))
|
||||
|
||||
def test_unset_cache_dir_keeps_the_temporary_one(self, monkeypatch):
|
||||
"""Without one configured, nothing changes for existing deployments."""
|
||||
monkeypatch.delenv("TRIVY_CACHE_DIR", raising=False)
|
||||
|
||||
provider = _make_provider()
|
||||
|
||||
assert os.path.isdir(provider._trivy_cache_dir)
|
||||
provider.cleanup()
|
||||
assert not os.path.isdir(provider._trivy_cache_dir)
|
||||
|
||||
def test_cleanup_removes_trivy_cache_dir(self):
|
||||
"""Test that cleanup removes the temporary Trivy cache directory."""
|
||||
provider = _make_provider()
|
||||
|
||||
@@ -4,6 +4,26 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.42.0] (Prowler v5.42.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- PostHog Toolbar support in development with separate ingestion and app hosts [(#12582)](https://github.com/prowler-cloud/prowler/pull/12582)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Scan Jobs onboarding tour no longer targets an unmounted In Progress row from other tabs [(#12705)](https://github.com/prowler-cloud/prowler/pull/12705)
|
||||
- Integration connection test polling now waits up to ~3 minutes instead of ~57 seconds before giving up, so it no longer reports a false failure on slower checks (e.g. Jira accounts with many projects) that were still going to succeed [(#12742)](https://github.com/prowler-cloud/prowler/pull/12742)
|
||||
- Scans page filter widths and action button styling, with Launch Scan and Import Findings grouped beside the tabs and sized consistently with Configure Mutelist [(#12781)](https://github.com/prowler-cloud/prowler/pull/12781)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- `nanoid` to 5.1.16, `js-yaml` to 4.3.1 and `postcss` to 8.5.23, plus transitive `hono`, `@hono/node-server`, `browserslist`, `qs`, `dompurify`, `brace-expansion`, `fast-uri`, `ip-address`, `mermaid`, `body-parser` and `@humanfs/node` to patched versions, resolving 40 npm audit advisories (21 high, 15 moderate, 4 low) [(#12758)](https://github.com/prowler-cloud/prowler/pull/12758)
|
||||
- `next` to 16.3.3, patching an unauthenticated remote code execution in the Image Optimization API when AVIF files are used (GHSA-2xp9-vwfh-vxw4) [(#12778)](https://github.com/prowler-cloud/prowler/pull/12778)
|
||||
- `sharp` to 0.35.4, patching two libheif vulnerabilities reachable through image decoding (GHSA-rgj7-g3m4-5g8c) [(#12778)](https://github.com/prowler-cloud/prowler/pull/12778)
|
||||
|
||||
---
|
||||
|
||||
## [1.41.0] (Prowler v5.41.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Integration connection test polling now waits up to ~3 minutes instead of ~57 seconds before giving up, so it no longer reports a false failure on slower checks (e.g. Jira accounts with many projects) that were still going to succeed
|
||||
@@ -1 +0,0 @@
|
||||
PostHog Toolbar support in development with separate ingestion and app hosts
|
||||
@@ -1 +0,0 @@
|
||||
Scan Jobs onboarding tour no longer targets an unmounted In Progress row from other tabs
|
||||
@@ -223,10 +223,9 @@ export function ImportFindingsModal() {
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(true)}
|
||||
className="w-full md:w-auto"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Import Findings
|
||||
</Button>
|
||||
|
||||
@@ -31,7 +31,7 @@ interface ScansFilterBarProps {
|
||||
onScanStatusChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const filterItemClass = "w-full md:w-[calc(50%-0.375rem)] xl:w-60";
|
||||
const filterItemClass = "w-full sm:max-w-[240px] sm:min-w-[180px] sm:flex-1";
|
||||
|
||||
export function ScansFilterBar({
|
||||
providers,
|
||||
@@ -67,33 +67,37 @@ export function ScansFilterBar({
|
||||
</div>
|
||||
|
||||
{showScheduleTypeFilter && (
|
||||
<Select value={scheduleType} onValueChange={onScheduleTypeChange}>
|
||||
<SelectTrigger aria-label="All Types" className={filterItemClass}>
|
||||
<SelectValue placeholder="All Types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{triggerFilterOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className={filterItemClass}>
|
||||
<Select value={scheduleType} onValueChange={onScheduleTypeChange}>
|
||||
<SelectTrigger aria-label="All Types">
|
||||
<SelectValue placeholder="All Types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{triggerFilterOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showStatusFilter && (
|
||||
<Select value={scanStatus} onValueChange={onScanStatusChange}>
|
||||
<SelectTrigger aria-label="All Statuses" className={filterItemClass}>
|
||||
<SelectValue placeholder="All Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statusFilterOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className={filterItemClass}>
|
||||
<Select value={scanStatus} onValueChange={onScanStatusChange}>
|
||||
<SelectTrigger aria-label="All Statuses">
|
||||
<SelectValue placeholder="All Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statusFilterOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -336,26 +336,35 @@ describe("ScansPageShell", () => {
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps launch scan with filters and mutelist with tabs", () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
it("keeps launch scan, import findings, and mutelist with tabs", () => {
|
||||
// Given
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
<ScansPageShell
|
||||
providers={providers}
|
||||
hasManageScansPermission
|
||||
hasManageIngestionsPermission
|
||||
>
|
||||
<div>Scans table</div>
|
||||
</ScansPageShell>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("group", { name: /scan filters and actions/i }),
|
||||
).toContainElement(screen.getByRole("button", { name: /launch scan/i }));
|
||||
expect(
|
||||
screen.getByRole("group", { name: /scan filters and actions/i }),
|
||||
).not.toContainElement(
|
||||
screen.getByRole("link", { name: /configure mutelist/i }),
|
||||
// Then
|
||||
const tabs = screen.getByRole("group", { name: /scan tabs/i });
|
||||
expect(tabs).toContainElement(
|
||||
screen.getByRole("button", { name: /launch scan/i }),
|
||||
);
|
||||
expect(screen.getByRole("group", { name: /scan tabs/i })).toContainElement(
|
||||
expect(tabs).toContainElement(
|
||||
screen.getByRole("button", { name: /import findings/i }),
|
||||
);
|
||||
expect(tabs).toContainElement(
|
||||
screen.getByRole("link", { name: /configure mutelist/i }),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("group", { name: /scan filters/i }),
|
||||
).toContainElement(screen.getByText("Shared scan filters"));
|
||||
});
|
||||
|
||||
it("shows the active scans count in the in progress tab", () => {
|
||||
|
||||
@@ -135,8 +135,8 @@ export function ScansPageShell({
|
||||
)}
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Scan filters and actions"
|
||||
className="flex flex-wrap items-center gap-3"
|
||||
aria-label="Scan filters"
|
||||
className="flex flex-wrap items-center gap-4"
|
||||
>
|
||||
<ScansFilterBar
|
||||
providers={providers}
|
||||
@@ -148,20 +148,6 @@ export function ScansPageShell({
|
||||
onScheduleTypeChange={filters.setScheduleType}
|
||||
onScanStatusChange={filters.setScanStatus}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
onClick={() => handleLaunchOpenChange(true)}
|
||||
disabled={launchDisabled}
|
||||
className="w-full md:w-auto"
|
||||
data-tour-id="view-first-scan-launch"
|
||||
>
|
||||
Launch Scan
|
||||
</Button>
|
||||
{isCloudEnvironment && hasManageIngestionsPermission && (
|
||||
<ImportFindingsModal />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCloudEnvironment && <CliImportBanner />}
|
||||
@@ -174,10 +160,10 @@ export function ScansPageShell({
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Scan tabs"
|
||||
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
className="flex flex-wrap items-center justify-between gap-3"
|
||||
>
|
||||
<TabsList
|
||||
className="overflow-x-auto"
|
||||
className="w-full overflow-x-auto sm:w-auto"
|
||||
data-tour-id="view-first-scan-tabs"
|
||||
>
|
||||
{Object.values(SCAN_JOBS_TAB).map((tab) => (
|
||||
@@ -186,7 +172,19 @@ export function ScansPageShell({
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="shrink-0">
|
||||
<div className="ml-auto flex w-full flex-wrap items-center gap-3 sm:w-auto">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleLaunchOpenChange(true)}
|
||||
disabled={launchDisabled}
|
||||
className="w-full sm:w-auto"
|
||||
data-tour-id="view-first-scan-launch"
|
||||
>
|
||||
Launch Scan
|
||||
</Button>
|
||||
{isCloudEnvironment && hasManageIngestionsPermission && (
|
||||
<ImportFindingsModal />
|
||||
)}
|
||||
<MutedFindingsConfigButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+14
-14
@@ -442,10 +442,10 @@
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "js-yaml",
|
||||
"from": "4.1.1",
|
||||
"to": "4.3.0",
|
||||
"from": "4.3.0",
|
||||
"to": "4.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2026-07-16T15:29:57.887Z"
|
||||
"generatedAt": "2026-09-08T07:45:48.316Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -491,17 +491,17 @@
|
||||
"section": "dependencies",
|
||||
"name": "nanoid",
|
||||
"from": "5.1.6",
|
||||
"to": "5.1.6",
|
||||
"to": "5.1.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-10T11:34:11.122Z"
|
||||
"generatedAt": "2026-09-08T07:45:48.316Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "next",
|
||||
"from": "16.2.9",
|
||||
"to": "16.2.11",
|
||||
"from": "16.2.11",
|
||||
"to": "16.3.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2026-07-24T08:32:45.227Z"
|
||||
"generatedAt": "2026-09-09T12:23:05.642Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -594,10 +594,10 @@
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "sharp",
|
||||
"from": "0.33.5",
|
||||
"to": "0.35.3",
|
||||
"from": "0.35.3",
|
||||
"to": "0.35.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2026-08-11T11:35:35.609Z"
|
||||
"generatedAt": "2026-09-09T12:39:08.649Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -930,10 +930,10 @@
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "postcss",
|
||||
"from": "8.4.38",
|
||||
"to": "8.5.14",
|
||||
"from": "8.5.14",
|
||||
"to": "8.5.23",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2026-05-14T10:09:04.901Z"
|
||||
"generatedAt": "2026-09-08T07:45:48.316Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
|
||||
+5
-5
@@ -90,14 +90,14 @@
|
||||
"driver.js": "1.4.0",
|
||||
"framer-motion": "11.18.2",
|
||||
"import-in-the-middle": "3.3.1",
|
||||
"js-yaml": "4.3.0",
|
||||
"js-yaml": "4.3.1",
|
||||
"jwt-decode": "4.0.0",
|
||||
"langchain": "1.4.0",
|
||||
"lucide-react": "0.543.0",
|
||||
"marked": "15.0.12",
|
||||
"modern-screenshot": "4.7.0",
|
||||
"nanoid": "5.1.6",
|
||||
"next": "16.2.11",
|
||||
"nanoid": "5.1.16",
|
||||
"next": "16.3.3",
|
||||
"next-auth": "5.0.0-beta.32",
|
||||
"next-themes": "0.2.1",
|
||||
"posthog-js": "1.407.2",
|
||||
@@ -109,7 +109,7 @@
|
||||
"recharts": "2.15.4",
|
||||
"require-in-the-middle": "8.0.1",
|
||||
"server-only": "0.0.1",
|
||||
"sharp": "0.35.3",
|
||||
"sharp": "0.35.4",
|
||||
"streamdown": "1.6.10",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
@@ -153,7 +153,7 @@
|
||||
"jsdom": "27.4.0",
|
||||
"knip": "6.3.1",
|
||||
"msw": "2.13.4",
|
||||
"postcss": "8.5.14",
|
||||
"postcss": "8.5.23",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-packagejson": "2.5.22",
|
||||
"prettier-plugin-tailwindcss": "0.6.14",
|
||||
|
||||
Generated
+456
-381
File diff suppressed because it is too large
Load Diff
+55
-19
@@ -18,21 +18,25 @@ overrides:
|
||||
"@react-aria/visually-hidden>react": "19.2.7"
|
||||
"@react-aria/interactions>react": "19.2.7"
|
||||
"lodash": "4.18.1"
|
||||
# sharp 0.33.x/0.34.x carry GHSA-f88m-g3jw-g9cj; next pulls 0.34.5 transitively.
|
||||
"sharp": "0.35.3"
|
||||
# Next.js 16.3.3 requests sharp ^0.35.3; resolve 0.35.4 to fix
|
||||
# GHSA-rgj7-g3m4-5g8c (libheif). This override controls resolution;
|
||||
# keep it aligned with the direct dependency in package.json.
|
||||
"sharp": "0.35.4"
|
||||
"lodash-es": "4.18.1"
|
||||
# GHSA-88fw-hqm2-52qc (CORS reflects any Origin with credentials) + 4 moderate
|
||||
# advisories (serve-static path traversal, Lambda Set-Cookie merge, body-limit
|
||||
# bypass, Lambda@Edge repeated-header loss), all fixed in 4.12.25, plus
|
||||
# CVE-2026-59896 (hono/jsx SSR context leak across concurrent requests; in NVD
|
||||
# but not yet in the npm audit feed), fixed in 4.12.27. Not 4.12.29: it is
|
||||
# still inside StepSecurity's 7-day npm cooldown gate.
|
||||
"hono": "4.12.28"
|
||||
"@hono/node-server": "1.19.14"
|
||||
# GHSA-88fw-hqm2-52qc (CORS reflects any Origin with credentials), CVE-2026-59896
|
||||
# (hono/jsx SSR context leak) and the 4.12.34 batch: CORS ReDoS via
|
||||
# Access-Control-Request-Headers, `memo()` SSR output retained across requests,
|
||||
# Language middleware algorithmic DoS, Proxy Helper keeping `Connection` headers.
|
||||
# Node adapter 1.19.17 fixes serve-static path traversal via `%5C` on Windows
|
||||
# (1.19.15 was published without provenance and trips `trustPolicy: no-downgrade`).
|
||||
"hono": "4.12.34"
|
||||
"@hono/node-server": "1.19.17"
|
||||
"@isaacs/brace-expansion": "5.0.1"
|
||||
"fast-xml-parser": "5.8.0"
|
||||
"serialize-javascript": "7.0.5"
|
||||
"postcss": "8.5.14"
|
||||
# GHSA-6g55-p6wh-862q (sourceMappingURL path traversal reads arbitrary .map files)
|
||||
# and its incomplete-fix follow-up when `from` is unset, both closed in 8.5.23.
|
||||
"postcss": "8.5.23"
|
||||
"esbuild": "0.28.1"
|
||||
"rollup@>=4": "4.59.0"
|
||||
# GHSA-fx2h-pf6j-xcff (server.fs.deny bypass on Windows alternate paths, high) +
|
||||
@@ -52,10 +56,11 @@ overrides:
|
||||
# fixed in 7.29.1. An override instead of `pnpm update` so the rest of the
|
||||
# babel/browserslist subtree keeps its existing lockfile resolutions.
|
||||
"@babel/core": "7.29.7"
|
||||
# Ephemeral cooldown pins: the @babel/helper-compilation-targets refresh pulls
|
||||
# browserslist-ecosystem releases newer than StepSecurity's 7-day npm cooldown.
|
||||
# Safe to drop after 2026-07-20.
|
||||
"browserslist": "4.28.2"
|
||||
# browserslist 4.28.7 fixes unbounded query-result cache growth (OOM) and an
|
||||
# uncaught crash / prototype write from untrusted browserslist-stats.json.
|
||||
# caniuse-lite and baseline-browser-mapping stay pinned so the babel subtree
|
||||
# does not float past StepSecurity's 7-day npm cooldown gate.
|
||||
"browserslist": "4.28.7"
|
||||
"caniuse-lite": "1.0.30001792"
|
||||
"baseline-browser-mapping": "2.10.29"
|
||||
"minimatch@<4": "3.1.4"
|
||||
@@ -63,7 +68,9 @@ overrides:
|
||||
"minimatch@>=10": "10.2.3"
|
||||
"ajv@<7": "6.14.0"
|
||||
"ajv@>=8": "8.18.0"
|
||||
"qs": "6.15.2"
|
||||
# 6.16.0 fixes the bracket-key comma array-limit bypass and DoS via an
|
||||
# attacker-controlled isBuffer.
|
||||
"qs": "6.16.0"
|
||||
# 8.2.2 dropped provenance attestation; 8.3.1+ restored it. Pinned to skip 8.2.2
|
||||
# under `trustPolicy: no-downgrade`.
|
||||
"express-rate-limit": "8.5.1"
|
||||
@@ -73,9 +80,38 @@ overrides:
|
||||
# but the override unifies the tree on a patched version.
|
||||
"uuid": "11.1.1"
|
||||
# GHSA-vxr8-fq34-vvx9 (+ several related XSS sanitization bypasses): DOMPurify < 3.4.9,
|
||||
# pulled in transitively via streamdown > mermaid (which wants ^3.3.1). Bumped to 3.4.11
|
||||
# for GHSA-cmwh-pvxp-8882 (permanent ALLOWED_ATTR pollution via setConfig()).
|
||||
"dompurify": "3.4.11"
|
||||
# pulled in transitively via streamdown > mermaid and posthog-js. 3.4.11 closed
|
||||
# GHSA-cmwh-pvxp-8882 (permanent ALLOWED_ATTR pollution via setConfig()); 3.4.13
|
||||
# also closes the CUSTOM_ELEMENT_HANDLING afterSanitizeElements bypass and the
|
||||
# IN_PLACE hook removal that left a detached subtree executable.
|
||||
"dompurify": "3.4.13"
|
||||
|
||||
# Advisories flagged by `pnpm audit` on 2026-09-08. Every pin below is the
|
||||
# oldest patched release and was published more than 7 days before that date,
|
||||
# so it clears StepSecurity's npm cooldown gate.
|
||||
# brace-expansion: three DoS advisories (exponential `{}` expansion, unbounded
|
||||
# expansion length OOM, unbounded intermediate arrays bypassing the
|
||||
# CVE-2026-14257 mitigation). 1.x via eslint > minimatch, 5.x via @sentry > glob.
|
||||
"brace-expansion@<2": "1.1.18"
|
||||
"brace-expansion@>=5": "5.0.9"
|
||||
# fast-uri (via ajv): host confusion through backslash authority delimiters,
|
||||
# failed IDN canonicalization and percent-encoded scheme normalization, plus SSRF
|
||||
# via malformed IPv6 normalization and repeated hostname percent-decoding.
|
||||
"fast-uri": "3.1.6"
|
||||
# ip-address (via express-rate-limit): SSRF / trust-boundary bypasses from
|
||||
# leading-zero octets, CIDR suffixes and IPv4-mapped / NAT64 misclassification.
|
||||
"ip-address": "10.3.1"
|
||||
# mermaid (via streamdown): prototype pollution in config APIs and Architecture
|
||||
# diagrams, CSS injection into sibling elements, XY Chart infinite loop and
|
||||
# radar diagram DoS.
|
||||
"mermaid": "11.16.1"
|
||||
# body-parser (via express): invalid `limit` silently disabled size enforcement.
|
||||
"body-parser": "2.3.0"
|
||||
# @humanfs/node (via eslint): recursive copy followed symlinks outside the tree.
|
||||
"@humanfs/node": "0.16.8"
|
||||
# js-yaml: quadratic CPU in `!!omap` resolution (CVE-2026-59870 not backported
|
||||
# to 4.3.0). Direct dep is already 4.3.1; the override lifts eslint's copy too.
|
||||
"js-yaml": "4.3.1"
|
||||
|
||||
# --- Level 1: Minimum Release Age ---
|
||||
# Packages must be published for at least 1 day before they can be installed.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -27,10 +27,10 @@ export class ScansPage extends BasePage {
|
||||
// The sidebar exposes its own icon-button labeled "Launch Scan"
|
||||
// (aria-label, wrapped in a Tooltip), so scoping by accessible name
|
||||
// alone hits a strict-mode duplicate. Scope to the page-shell's
|
||||
// filters-and-actions group, which only contains the visible-text
|
||||
// tabs-and-actions group, which only contains the visible-text
|
||||
// Launch Scan button.
|
||||
this.launchScanButton = page
|
||||
.getByRole("group", { name: /scan filters and actions/i })
|
||||
.getByRole("group", { name: /scan tabs/i })
|
||||
.getByRole("button", { name: /^Launch Scan$/i });
|
||||
this.launchScanDialog = page.getByRole("dialog");
|
||||
// The modal renders the providers picker as the shared MultiSelect-based
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
### Flow Steps
|
||||
|
||||
1. Navigate to Scans page
|
||||
2. Click "Launch Scan" to open the launch scan modal
|
||||
2. Click "Launch Scan" beside the scan tabs to open the launch scan modal
|
||||
3. Open the Cloud Account selector and choose the entry whose text contains E2E_AWS_PROVIDER_ACCOUNT_ID
|
||||
4. Optionally fill Scan Note
|
||||
5. Click "Launch Scan" in the modal
|
||||
@@ -46,6 +46,7 @@
|
||||
|
||||
- Scans page loads correctly
|
||||
- Launch Scan modal opens correctly
|
||||
- The page-level "Launch Scan" button is located in the "Scan tabs" group, distinct from the sidebar action
|
||||
- Cloud Account select is available and lists the configured provider UID
|
||||
- "Launch Scan" button is rendered and enabled when form is valid
|
||||
- Success toast message: "The scan was launched successfully."
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import sys
|
||||
|
||||
import boto3
|
||||
from botocore.session import Session as BotocoreSession
|
||||
|
||||
# Logging config
|
||||
logging.basicConfig(
|
||||
@@ -13,91 +14,286 @@ logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
)
|
||||
|
||||
regions_by_service = {"services": {}}
|
||||
# AWS partitions that the SSM global-infrastructure parameters do not publish.
|
||||
# Their availability comes from the endpoints.json bundled with botocore, which
|
||||
# is offline data and needs neither credentials nor network access.
|
||||
ISO_PARTITIONS = ("aws-iso", "aws-iso-b", "aws-iso-e", "aws-iso-f")
|
||||
|
||||
logging.info("Recovering AWS Regions by Service")
|
||||
client = boto3.client("ssm", region_name="us-east-1")
|
||||
get_parameters_by_path_paginator = client.get_paginator("get_parameters_by_path")
|
||||
# Get all AWS Available Services
|
||||
for page in get_parameters_by_path_paginator.paginate(
|
||||
Path="/aws/service/global-infrastructure/services"
|
||||
):
|
||||
for service in page["Parameters"]:
|
||||
regions_by_service["services"][service["Value"]] = {}
|
||||
# Get all AWS Regions for the specific service
|
||||
regions = {"aws": [], "aws-cn": [], "aws-eusc": [], "aws-us-gov": []}
|
||||
for page in get_parameters_by_path_paginator.paginate(
|
||||
Path="/aws/service/global-infrastructure/services/"
|
||||
+ service["Value"]
|
||||
+ "/regions"
|
||||
):
|
||||
for region in page["Parameters"]:
|
||||
if "cn" in region["Value"]:
|
||||
regions["aws-cn"].append(region["Value"])
|
||||
elif "eusc" in region["Value"]:
|
||||
regions["aws-eusc"].append(region["Value"])
|
||||
elif "gov" in region["Value"]:
|
||||
regions["aws-us-gov"].append(region["Value"])
|
||||
else:
|
||||
regions["aws"].append(region["Value"])
|
||||
# Sort regions per partition
|
||||
regions["aws"] = sorted(regions["aws"])
|
||||
regions["aws-cn"] = sorted(regions["aws-cn"])
|
||||
regions["aws-eusc"] = sorted(regions["aws-eusc"])
|
||||
regions["aws-us-gov"] = sorted(regions["aws-us-gov"])
|
||||
regions_by_service["services"][service["Value"]]["regions"] = regions
|
||||
# Cost Explorer: botocore keys it by its endpoint prefix "ce", while the matrix
|
||||
# (and the boto3 client name) calls it "costexplorer". Explicit rename override,
|
||||
# since no boto3 service model resolves the "ce" prefix.
|
||||
ISO_ENDPOINT_PREFIX_RENAMES = {"ce": "costexplorer"}
|
||||
|
||||
# Include the regions for the subservices and the services not present
|
||||
logging.info("Updating subservices and the services not present in the original matrix")
|
||||
# macie2 --> macie
|
||||
regions_by_service["services"]["macie2"] = regions_by_service["services"]["macie"]
|
||||
# bedrock-agent is not in SSM, and has different availability than bedrock
|
||||
# See: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-supported.html
|
||||
regions_by_service["services"]["bedrock-agent"] = {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1",
|
||||
],
|
||||
# "transcribestreaming" is the streaming endpoint of Amazon Transcribe. The
|
||||
# "transcribe" prefix is already present in the same partitions with the same
|
||||
# regions, so mapping it would only duplicate data. Ignoring it is a deliberate
|
||||
# decision, not a resolution failure.
|
||||
ISO_IGNORED_ENDPOINT_PREFIXES = {"transcribestreaming"}
|
||||
|
||||
# A service whose only endpoint in a partition is the partition-wide pseudo
|
||||
# endpoint (for example "aws-iso-global") gets every region of that partition,
|
||||
# matching how the matrix already records iam, organizations, route53 and
|
||||
# support for aws and aws-us-gov. Cost Explorer is the exception: the matrix
|
||||
# records it as a single-region service (aws: us-east-1, aws-cn: cn-northwest-1),
|
||||
# so it only gets the region declared in the endpoint's credentialScope.
|
||||
ISO_SINGLE_REGION_PARTITION_GLOBAL_SERVICES = {"costexplorer"}
|
||||
|
||||
|
||||
def get_regions_by_service_from_ssm() -> dict:
|
||||
"""Get the AWS services and their regions for the partitions published in
|
||||
the SSM global-infrastructure parameters: aws, aws-cn, aws-eusc and
|
||||
aws-us-gov.
|
||||
|
||||
Returns:
|
||||
dict: The AWS regions matrix, keyed by service name.
|
||||
"""
|
||||
regions_by_service = {"services": {}}
|
||||
|
||||
logging.info("Recovering AWS Regions by Service")
|
||||
client = boto3.client("ssm", region_name="us-east-1")
|
||||
get_parameters_by_path_paginator = client.get_paginator("get_parameters_by_path")
|
||||
# Get all AWS Available Services
|
||||
for page in get_parameters_by_path_paginator.paginate(
|
||||
Path="/aws/service/global-infrastructure/services"
|
||||
):
|
||||
for service in page["Parameters"]:
|
||||
regions_by_service["services"][service["Value"]] = {}
|
||||
# Get all AWS Regions for the specific service
|
||||
regions = {
|
||||
"aws": [],
|
||||
"aws-cn": [],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": [],
|
||||
"aws-iso": [],
|
||||
"aws-iso-b": [],
|
||||
"aws-iso-e": [],
|
||||
"aws-iso-f": [],
|
||||
}
|
||||
for page in get_parameters_by_path_paginator.paginate(
|
||||
Path="/aws/service/global-infrastructure/services/"
|
||||
+ service["Value"]
|
||||
+ "/regions"
|
||||
):
|
||||
for region in page["Parameters"]:
|
||||
if "cn" in region["Value"]:
|
||||
regions["aws-cn"].append(region["Value"])
|
||||
elif "eusc" in region["Value"]:
|
||||
regions["aws-eusc"].append(region["Value"])
|
||||
elif "gov" in region["Value"]:
|
||||
regions["aws-us-gov"].append(region["Value"])
|
||||
else:
|
||||
regions["aws"].append(region["Value"])
|
||||
# Sort regions per partition
|
||||
regions["aws"] = sorted(regions["aws"])
|
||||
regions["aws-cn"] = sorted(regions["aws-cn"])
|
||||
regions["aws-eusc"] = sorted(regions["aws-eusc"])
|
||||
regions["aws-us-gov"] = sorted(regions["aws-us-gov"])
|
||||
regions_by_service["services"][service["Value"]]["regions"] = regions
|
||||
|
||||
return regions_by_service
|
||||
|
||||
|
||||
def add_subservices_and_missing_services(regions_by_service: dict) -> None:
|
||||
"""Include the regions for the subservices and the services not present in
|
||||
the original matrix."""
|
||||
logging.info(
|
||||
"Updating subservices and the services not present in the original matrix"
|
||||
)
|
||||
# macie2 --> macie
|
||||
regions_by_service["services"]["macie2"] = regions_by_service["services"]["macie"]
|
||||
# bedrock-agent is not in SSM, and has different availability than bedrock
|
||||
# See: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-supported.html
|
||||
regions_by_service["services"]["bedrock-agent"] = {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
# cognito --> cognito-idp
|
||||
regions_by_service["services"]["cognito"] = regions_by_service["services"][
|
||||
"cognito-idp"
|
||||
]
|
||||
# opensearch --> es
|
||||
regions_by_service["services"]["opensearch"] = regions_by_service["services"]["es"]
|
||||
# elbv2 --> elb
|
||||
regions_by_service["services"]["elbv2"] = regions_by_service["services"]["elb"]
|
||||
# wafv2 --> waf
|
||||
regions_by_service["services"]["wafv2"] = regions_by_service["services"]["waf"]
|
||||
# wellarchitected --> wellarchitectedtool
|
||||
regions_by_service["services"]["wellarchitected"] = regions_by_service["services"][
|
||||
"wellarchitectedtool"
|
||||
]
|
||||
# sesv2 --> ses
|
||||
regions_by_service["services"]["sesv2"] = regions_by_service["services"]["ses"]
|
||||
# cognito --> cognito-idp
|
||||
regions_by_service["services"]["cognito"] = regions_by_service["services"][
|
||||
"cognito-idp"
|
||||
]
|
||||
# opensearch --> es
|
||||
regions_by_service["services"]["opensearch"] = regions_by_service["services"]["es"]
|
||||
# elbv2 --> elb
|
||||
regions_by_service["services"]["elbv2"] = regions_by_service["services"]["elb"]
|
||||
# wafv2 --> waf
|
||||
regions_by_service["services"]["wafv2"] = regions_by_service["services"]["waf"]
|
||||
# wellarchitected --> wellarchitectedtool
|
||||
regions_by_service["services"]["wellarchitected"] = regions_by_service["services"][
|
||||
"wellarchitectedtool"
|
||||
]
|
||||
# sesv2 --> ses
|
||||
regions_by_service["services"]["sesv2"] = regions_by_service["services"]["ses"]
|
||||
|
||||
# Write to file
|
||||
parsed_matrix_regions_aws = f"{os.path.dirname(os.path.realpath(__name__))}/prowler/providers/aws/aws_regions_by_service.json"
|
||||
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")
|
||||
|
||||
def get_endpoint_prefix_to_services() -> dict:
|
||||
"""Map every botocore endpoint prefix to the set of boto3 service (client)
|
||||
names using it.
|
||||
|
||||
botocore's endpoints.json keys services by endpoint prefix, while the matrix
|
||||
keys them by the boto3/SSM service name. The mapping is derived from the SDK
|
||||
itself instead of being hand-written, so it stays correct as the SDK evolves
|
||||
(monitoring -> cloudwatch, elasticloadbalancing -> elb and elbv2, states ->
|
||||
stepfunctions, api.ecr -> ecr, ...).
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping each endpoint prefix to a set of service names.
|
||||
"""
|
||||
session = BotocoreSession()
|
||||
endpoint_prefix_to_services = {}
|
||||
for service_name in session.get_available_services():
|
||||
endpoint_prefix = session.get_service_model(service_name).endpoint_prefix
|
||||
endpoint_prefix_to_services.setdefault(endpoint_prefix, set()).add(service_name)
|
||||
return endpoint_prefix_to_services
|
||||
|
||||
|
||||
def resolve_matrix_services(
|
||||
endpoint_prefix: str, endpoint_prefix_to_services: dict, services: dict
|
||||
) -> set:
|
||||
"""Resolve a botocore endpoint prefix to the matrix service names it stands
|
||||
for.
|
||||
|
||||
Args:
|
||||
- endpoint_prefix: The botocore endpoint prefix.
|
||||
- endpoint_prefix_to_services: The map returned by get_endpoint_prefix_to_services.
|
||||
- services: The services of the AWS regions matrix.
|
||||
|
||||
Returns:
|
||||
set: The matrix service names, empty when the prefix does not resolve.
|
||||
"""
|
||||
renamed_service = ISO_ENDPOINT_PREFIX_RENAMES.get(endpoint_prefix)
|
||||
if renamed_service:
|
||||
return {renamed_service} & set(services)
|
||||
|
||||
service_names = endpoint_prefix_to_services.get(endpoint_prefix, set()) & set(
|
||||
services
|
||||
)
|
||||
if not service_names and endpoint_prefix in services:
|
||||
service_names = {endpoint_prefix}
|
||||
return service_names
|
||||
|
||||
|
||||
def get_partition_global_service_regions(
|
||||
service_names: set, service_data: dict, partition_regions: list
|
||||
) -> list:
|
||||
"""Get the regions of a service whose only endpoint in the partition is the
|
||||
partition-wide pseudo endpoint (for example "aws-iso-global"), which is not
|
||||
a region and must never be recorded as one.
|
||||
|
||||
Returns:
|
||||
list: Every region of the partition, or only the credentialScope region
|
||||
for the services the matrix records as single-region ones.
|
||||
"""
|
||||
partition_endpoint = service_data.get("partitionEndpoint")
|
||||
credential_scope_region = (
|
||||
service_data.get("endpoints", {})
|
||||
.get(partition_endpoint, {})
|
||||
.get("credentialScope", {})
|
||||
.get("region")
|
||||
)
|
||||
if service_names & ISO_SINGLE_REGION_PARTITION_GLOBAL_SERVICES:
|
||||
if credential_scope_region in partition_regions:
|
||||
return [credential_scope_region]
|
||||
return []
|
||||
return list(partition_regions)
|
||||
|
||||
|
||||
def add_iso_partitions_regions(regions_by_service: dict) -> None:
|
||||
"""Fill the aws-iso, aws-iso-b, aws-iso-e and aws-iso-f regions of every
|
||||
service from the endpoints.json bundled with botocore.
|
||||
|
||||
It runs after the subservices and the services not present in the original
|
||||
matrix have been added, so it sees the final set of services: the aliases
|
||||
sharing a single dict and the hand-written bedrock-agent entry all get their
|
||||
ISO partition keys.
|
||||
|
||||
Raises:
|
||||
ValueError: If an endpoint prefix present in an ISO partition does not
|
||||
resolve to a matrix service and is not explicitly ignored.
|
||||
"""
|
||||
logging.info("Updating the ISO partitions regions from the botocore endpoints")
|
||||
services = regions_by_service["services"]
|
||||
endpoints_data = BotocoreSession().get_data("endpoints")
|
||||
endpoint_prefix_to_services = get_endpoint_prefix_to_services()
|
||||
|
||||
# Every service carries every partition key, so the matrix stays rectangular
|
||||
# even for the services with no presence at all in the ISO partitions.
|
||||
for service in services.values():
|
||||
for partition in ISO_PARTITIONS:
|
||||
service["regions"].setdefault(partition, [])
|
||||
|
||||
for partition_data in endpoints_data["partitions"]:
|
||||
partition = partition_data["partition"]
|
||||
if partition not in ISO_PARTITIONS:
|
||||
continue
|
||||
partition_regions = sorted(partition_data.get("regions", {}))
|
||||
for endpoint_prefix, service_data in partition_data.get("services", {}).items():
|
||||
if endpoint_prefix in ISO_IGNORED_ENDPOINT_PREFIXES:
|
||||
continue
|
||||
service_names = resolve_matrix_services(
|
||||
endpoint_prefix, endpoint_prefix_to_services, services
|
||||
)
|
||||
if not service_names:
|
||||
raise ValueError(
|
||||
f"The botocore endpoint prefix '{endpoint_prefix}', present in the "
|
||||
f"'{partition}' partition, does not resolve to any service of the "
|
||||
"AWS regions matrix. Dropping it silently would leave the service "
|
||||
"out of the scans, so either add the prefix to "
|
||||
"ISO_ENDPOINT_PREFIX_RENAMES with the matrix service name it "
|
||||
"corresponds to, or add it to ISO_IGNORED_ENDPOINT_PREFIXES if it "
|
||||
"must not be mapped."
|
||||
)
|
||||
# Keep only the endpoints that are real regions of the partition,
|
||||
# which drops the fips-* and the partition-wide pseudo endpoints.
|
||||
regions = sorted(
|
||||
set(service_data.get("endpoints", {})) & set(partition_regions)
|
||||
)
|
||||
if not regions:
|
||||
regions = get_partition_global_service_regions(
|
||||
service_names, service_data, partition_regions
|
||||
)
|
||||
for service_name in service_names:
|
||||
services[service_name]["regions"][partition] = list(regions)
|
||||
|
||||
|
||||
def write_regions_by_service(regions_by_service: dict) -> None:
|
||||
"""Write the AWS regions matrix to the file read by the AWS provider."""
|
||||
repository_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
parsed_matrix_regions_aws = (
|
||||
f"{repository_root}/prowler/providers/aws/aws_regions_by_service.json"
|
||||
)
|
||||
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")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
regions_by_service = get_regions_by_service_from_ssm()
|
||||
add_subservices_and_missing_services(regions_by_service)
|
||||
add_iso_partitions_regions(regions_by_service)
|
||||
write_regions_by_service(regions_by_service)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user