Merge branch 'PROWLER-1391-provider-contract-dynamic-discovery' into PROWLER-1771-public-dynamic-provider-class-resolver

# Conflicts:
#	prowler/CHANGELOG.md
This commit is contained in:
StylusFrost committed 2026-05-31 20:05:35 +02:00
commit fe821a41ea
306 files changed
+19238 -2270

No files matched your search

+8 -1
View File
@@ -11,7 +11,14 @@ envs = "wt step copy-ignored"
[[pre-start]]
deps = "uv sync"
# Block 3: reminder - last visible output before `wt switch` returns.
# Block 3: prepare pnpm via corepack.
[[pre-start]]
corepack-enable = "corepack enable"
[[pre-start]]
corepack-install = "cd ui && corepack install"
# Block 4: reminder - last visible output before `wt switch` returns.
# Hooks can't mutate the parent shell, so venv activation is manual.
[[pre-start]]
reminder = "echo '>> Reminder: activate the venv in this shell with: source .venv/bin/activate'"
+50 -1
View File
@@ -540,7 +540,55 @@ jobs:
with:
flags: prowler-py${{ matrix.python-version }}-vercel
files: ./vercel_coverage.xml
# Scaleway Provider
- name: Check if Scaleway files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-scaleway
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
./prowler/**/scaleway/**
./tests/**/scaleway/**
./uv.lock
- name: Run Scaleway tests
if: steps.changed-scaleway.outputs.any_changed == 'true'
run: uv run pytest -n auto --cov=./prowler/providers/scaleway --cov-report=xml:scaleway_coverage.xml tests/providers/scaleway
- name: Upload Scaleway coverage to Codecov
if: steps.changed-scaleway.outputs.any_changed == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler-py${{ matrix.python-version }}-scaleway
files: ./scaleway_coverage.xml
# StackIT Provider
- name: Check if StackIT files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-stackit
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
./prowler/**/stackit/**
./tests/**/stackit/**
./uv.lock
- name: Run StackIT tests
if: steps.changed-stackit.outputs.any_changed == 'true'
run: uv run pytest -n auto --cov=./prowler/providers/stackit --cov-report=xml:stackit_coverage.xml tests/providers/stackit
- name: Upload StackIT coverage to Codecov
if: steps.changed-stackit.outputs.any_changed == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler-py${{ matrix.python-version }}-stackit
files: ./stackit_coverage.xml
# External Provider (dynamic loading)
- name: Check if External Provider files changed
if: steps.check-changes.outputs.any_changed == 'true'
@@ -560,13 +608,14 @@ jobs:
- name: Upload External Provider coverage to Codecov
if: steps.changed-external.outputs.any_changed == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler-py${{ matrix.python-version }}-external
files: ./external_coverage.xml
# Lib
- name: Check if Lib files changed
if: steps.check-changes.outputs.any_changed == 'true'
+1 -1
View File
@@ -172,7 +172,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '24.13.0'
node-version-file: 'ui/.nvmrc'
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
+2 -3
View File
@@ -16,7 +16,6 @@ concurrency:
env:
UI_WORKING_DIR: ./ui
NODE_VERSION: "24.13.0"
permissions: {}
@@ -93,11 +92,11 @@ jobs:
ui/vitest.config.ts
ui/vitest.setup.ts
- name: Setup Node.js ${{ env.NODE_VERSION }}
- name: Setup Node.js
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: 'ui/.nvmrc'
- name: Setup pnpm
if: steps.check-changes.outputs.any_changed == 'true'
+2 -1
View File
@@ -122,6 +122,7 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
| Vercel | 26 | 6 | 0 | 8 | Official | UI, API, CLI |
| Okta | 1 | 1 | 0 | 1 | Official | CLI |
| Scaleway [Contact us](https://prowler.com/contact) | 1 | 1 | 0 | 1 | Unofficial | CLI |
| StackIT [Contact us](https://prowler.com/contact) | 4 | 1 | 0 | 1 | Unofficial | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
> [!Note]
@@ -293,7 +294,7 @@ python prowler-cli.py -v
# 🛡️ GitHub Action
The official **Prowler GitHub Action** runs Prowler scans in your GitHub workflows using the official [`prowlercloud/prowler`](https://hub.docker.com/r/prowlercloud/prowler) Docker image. Scans run on any [supported provider](https://docs.prowler.com/user-guide/providers/), with optional [`--push-to-cloud`](https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings) to send findings to Prowler Cloud and optional SARIF upload so findings show up in the repo's **Security → Code scanning** tab and as inline PR annotations.
The official **Prowler GitHub Action** runs Prowler scans in your GitHub workflows using the official [`prowlercloud/prowler`](https://hub.docker.com/r/prowlercloud/prowler) Docker image. Scans run on any [supported provider](https://docs.prowler.com/user-guide/providers/), with optional [`--push-to-cloud`](https://docs.prowler.com/user-guide/tutorials/prowler-import-findings) to send findings to Prowler Cloud and optional SARIF upload so findings show up in the repo's **Security → Code scanning** tab and as inline PR annotations.
```yaml
name: Prowler IaC Scan
+2 -2
View File
@@ -22,7 +22,7 @@ inputs:
required: false
default: json-ocsf
push-to-cloud:
description: Push scan findings to Prowler Cloud. Requires the PROWLER_CLOUD_API_KEY environment variable. See https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings#using-the-cli
description: Push scan findings to Prowler Cloud. Requires the PROWLER_CLOUD_API_KEY environment variable. See https://docs.prowler.com/user-guide/tutorials/prowler-import-findings#using-the-cli
required: false
default: "false"
flags:
@@ -299,7 +299,7 @@ runs:
echo ""
echo "**Get started in 3 steps:**"
echo "1. Create an account at [cloud.prowler.com](https://cloud.prowler.com)"
echo "2. Generate a Prowler Cloud API key ([docs](https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings#using-the-cli))"
echo "2. Generate a Prowler Cloud API key ([docs](https://docs.prowler.com/user-guide/tutorials/prowler-import-findings#using-the-cli))"
echo "3. Add \`PROWLER_CLOUD_API_KEY\` to your GitHub secrets and set \`push-to-cloud: true\` on this action"
echo ""
echo "See [prowler.com/pricing](https://prowler.com/pricing) for plan details."
+2 -1
View File
@@ -2,11 +2,12 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.30.0] (Prowler UNRELEASED)
## [1.30.0] (Prowler v5.29.0)
### 🔄 Changed
- Scan finding ingestion: bulk-resolve `Resource`/`ResourceTag` rows, replace per-mapping `SELECT FOR UPDATE` with deferred `ResourceTagMapping.bulk_create(ignore_conflicts=True)`, wrap each micro-batch in a single `rls_transaction`, and raise `SCAN_DB_BATCH_SIZE` to 1000 [(#11249)](https://github.com/prowler-cloud/prowler/pull/11249)
- Faster `GET /api/v1/finding-groups/latest` aggregation on tenants where one recent scan holds most findings [(#11380)](https://github.com/prowler-cloud/prowler/pull/11380)
---
+5 -2
View File
@@ -7484,14 +7484,17 @@ class FindingGroupViewSet(BaseRLSViewSet):
def _get_latest_findings_per_provider(self, filtered_queryset):
"""Keep only findings from each provider's most recent completed scan."""
latest_scan_ids = (
# Materialize to a literal IN list. Left as a subquery, Postgres can't
# estimate the match count and picks a serial nested loop on
# resource_finding_mappings when one scan dominates findings
latest_scan_ids = list(
Scan.objects.filter(
tenant_id=self.request.tenant_id,
state=StateChoices.COMPLETED,
)
.order_by("provider_id", "-completed_at", "-inserted_at")
.distinct("provider_id")
.values("id")
.values_list("id", flat=True)
)
return filtered_queryset.filter(scan_id__in=latest_scan_ids)
+730
View File
@@ -0,0 +1,730 @@
---
title: 'StackIT Provider'
---
This page details the [StackIT Cloud](https://www.stackit.de/) provider implementation in Prowler.
By default, Prowler audits a single StackIT project per scan. To configure it, provide the project ID and either a service account key file path or inline service account key JSON.
## StackIT Provider Classes Architecture
The StackIT provider implementation follows the general [Provider structure](/developer-guide/provider). This section focuses on the StackIT-specific implementation, highlighting how the generic provider concepts are realized for StackIT in Prowler. For a full overview of the provider pattern, base classes, and extension guidelines, see [Provider documentation](/developer-guide/provider).
### `StackitProvider` (Main Class)
- **Location:** [`prowler/providers/stackit/stackit_provider.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/stackit/stackit_provider.py)
- **Base Class:** Inherits from `Provider` (see [base class details](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/common/provider.py)).
- **Purpose:** Central orchestrator for StackIT-specific logic, API authentication, credential validation, and configuration.
- **Key StackIT Responsibilities:**
- Initializes StackIT SDK authentication via a service account key file or inline service account key JSON. The SDK mints and refreshes access tokens internally.
- Validates the service account credentials and project ID (UUID format validation).
- Loads and manages configuration, mutelist, and fixer settings.
- Provides properties and methods for downstream StackIT service classes to access credentials, identity, and configuration data.
### Data Models
- **Location:** [`prowler/providers/stackit/models.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/stackit/models.py)
- **Purpose:** Define structured data for StackIT identity and output configuration.
- **Key StackIT Models:**
- `StackITIdentityInfo`: Holds StackIT identity metadata, including project ID and project name (fetched automatically from Resource Manager API).
- `StackITOutputOptions`: Customizes default output filenames so StackIT reports include the audited project ID.
- IaaS resource models such as `SecurityGroup` and `SecurityGroupRule` are defined in the IaaS service module.
### StackIT Services
- **Location:** [`prowler/providers/stackit/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/stackit/services)
- **Purpose:** Implement StackIT service clients and resource collection logic following the generic [service pattern](/developer-guide/services#service-base-class).
- **Current Implementation:** The `IaaSService` collects security groups, rules, and network interface usage across supported StackIT regions.
### Exception Handling
- **Location:** [`prowler/providers/stackit/exceptions/exceptions.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/stackit/exceptions/exceptions.py)
- **Purpose:** Custom exception classes for StackIT-specific error handling, such as credential validation, API connection, and configuration errors.
- **Key Exception Classes:**
- `StackITBaseException`: Base exception for all StackIT provider errors.
- `StackITCredentialsError`: Raised when credentials are invalid or missing.
- `StackITInvalidProjectIdError`: Raised when project ID is invalid or not in UUID format.
- `StackITAPIError`: Raised when StackIT API calls fail.
## Authentication
### Service Account Creation and Key Generation
StackIT uses service account keys for API authentication. Service account keys are RSA key-pair based and provide secure, short-lived access tokens.
### Creating a Service Account Key
#### Method 1: Via StackIT Portal
1. **Navigate to Service Accounts**
- Go to the [StackIT Portal](https://portal.stackit.cloud/)
- Select your project
- Click on **Service Accounts** in the left sidebar
2. **Create or Select Service Account**
- If you don't have a service account, click **Create Service Account**
- Provide a name and description
- Assign necessary permissions:
- For IaaS security checks: `iaas.viewer` or `project.owner`
- For comprehensive audits: `project.owner`
3. **Generate Service Account Key**
- Select your service account
- Navigate to **Service Account Keys**
- Click **Create key**
- Choose one of the following options:
- **STACKIT-generated key pair** (Recommended): Let STACKIT automatically generate an RSA key-pair
- **User-provided key pair**: Upload your own RSA 2048 public key
4. **Download and Save the Key**
- Download the generated service account key file (JSON format)
- **Important**: Save the key securely - it contains your private key and will only be available once
- Store the key file in a secure location (e.g., `~/.stackit/sa_key.json`)
#### Method 2: Via StackIT CLI
```bash
# Install STACKIT CLI (if not already installed)
# Follow instructions at: https://github.com/stackitcloud/stackit-cli
# Create service account key (STACKIT-generated)
stackit service-account key create --email my-service-account@example.com
# Or create with your own RSA 2048 public key
# First, generate your RSA key pair:
openssl genrsa -out private-key.pem 2048
openssl rsa -in private-key.pem -pubout -out public-key.pem
# Then create the key with your public key:
stackit service-account key create \
--email my-service-account@example.com \
--public-key "$(cat public-key.pem)"
```
### Finding Your Project ID
Your StackIT project ID is a UUID that can be found:
1. In the StackIT Portal URL when viewing your project: `https://portal.stackit.cloud/projects/{PROJECT_ID}/...`
2. In the project settings page
3. Using the StackIT CLI: `stackit project list`
### Passing the Service Account Key to Prowler
Prowler accepts the service account credentials in two equivalent forms; both go through the same StackIT SDK flow and refresh access tokens internally.
#### Option 1: Key File Path (key persisted on disk)
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
export STACKIT_PROJECT_ID="12345678-1234-1234-1234-123456789abc"
prowler stackit
```
Or as CLI flags:
```bash
prowler stackit \
--stackit-service-account-key-path ~/.stackit/sa-key.json \
--stackit-project-id 12345678-1234-1234-1234-123456789abc
```
#### Option 2: Inline Key Content (CI/CD, secret managers)
```bash
export STACKIT_SERVICE_ACCOUNT_KEY="$(vault kv get -field=key stackit/sa)"
export STACKIT_PROJECT_ID="12345678-1234-1234-1234-123456789abc"
prowler stackit
```
Prefer the environment variable over the matching `--stackit-service-account-key` CLI flag; passing the secret on the command line leaks it through process listings and shell history.
### Credential Lookup Order
Prowler resolves credentials in this order:
1. **Command-line arguments**:
- `--stackit-service-account-key`
- `--stackit-service-account-key-path`
- `--stackit-project-id`
2. **Environment variables**:
- `STACKIT_SERVICE_ACCOUNT_KEY`
- `STACKIT_SERVICE_ACCOUNT_KEY_PATH`
- `STACKIT_PROJECT_ID`
When both the inline key and the key file path are set, the inline content takes precedence.
## Configuration
### Command-Line Arguments
StackIT-specific command-line arguments:
| Argument | Description | Required | Default |
|----------|-------------|----------|---------|
| `--stackit-service-account-key-path` | Path to a StackIT service account key JSON file | Yes* | `$STACKIT_SERVICE_ACCOUNT_KEY_PATH` |
| `--stackit-service-account-key` | Inline JSON content of a StackIT service account key (preferred env var: `STACKIT_SERVICE_ACCOUNT_KEY`) | Yes* | `$STACKIT_SERVICE_ACCOUNT_KEY` |
| `--stackit-project-id` | StackIT project ID (UUID format) | Yes* | `$STACKIT_PROJECT_ID` |
| `--stackit-region` | StackIT region(s) to scan | No | All available regions |
\* Required unless provided via environment variables.
### Input Validation
The StackIT provider performs comprehensive input validation:
- **Service Account Credentials**:
- At least one of `service_account_key_path` (file path) or `service_account_key` (inline JSON) must be supplied; both empty raises `StackITNonExistentTokenError`
- When both are provided the inline content takes precedence
- The key file path is logged as-is; the inline content is redacted in the credentials box
- **Project ID**:
- Must not be empty
- Must be a valid UUID format (e.g., `12345678-1234-1234-1234-123456789abc`)
- Validated using Python's UUID constructor
Invalid credentials will result in clear error messages before any API calls are made.
## Available Services
### IaaS (Infrastructure as a Service)
- **Service Class:** `IaaSService`
- **Location:** [`prowler/providers/stackit/services/iaas/iaas_service.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/stackit/services/iaas/iaas_service.py)
- **SDK:** Uses the [stackit-iaas](https://pypi.org/project/stackit-iaas/) Python SDK
- **Purpose:** Manages IaaS resources including security groups, servers, and network interfaces.
**Supported Resources:**
- Security Groups and Rules
- Servers (Virtual Machines)
- Network Interfaces (NICs)
**Key Features:**
- Automatic discovery of all security groups in the project
- Security rule parsing with support for unrestricted access detection
- Network interface analysis to determine whether security groups are in use
- By default, reports only security groups attached to at least one NIC; `--scan-unused-services` includes unused security groups too
## Available Checks
The StackIT provider currently implements 4 security checks focused on network security:
### 1. iaas_security_group_ssh_unrestricted
- **Severity:** High
- **Description:** Detects security groups that allow unrestricted SSH access (port 22) from the internet.
- **Risk:** Unrestricted SSH access increases the attack surface and risk of brute-force attacks.
- **Detection Logic:**
- Checks for ingress rules allowing TCP port 22
- Flags rules with `ip_range=None` or `ip_range="0.0.0.0/0"` or `ip_range="::/0"`
- Reports security groups attached to NICs by default, or all security groups when `--scan-unused-services` is enabled
### 2. iaas_security_group_rdp_unrestricted
- **Severity:** High
- **Description:** Detects security groups that allow unrestricted RDP access (port 3389) from the internet.
- **Risk:** Unrestricted RDP access enables potential unauthorized remote desktop access.
- **Detection Logic:**
- Checks for ingress rules allowing TCP port 3389
- Flags unrestricted IP ranges (None, 0.0.0.0/0, ::/0)
- Reports security groups attached to NICs by default, or all security groups when `--scan-unused-services` is enabled
### 3. iaas_security_group_database_unrestricted
- **Severity:** High
- **Description:** Detects security groups that allow unrestricted access to common database ports.
- **Monitored Ports:**
- MySQL: 3306
- PostgreSQL: 5432
- MongoDB: 27017
- Redis: 6379
- SQL Server: 1433
- CouchDB: 5984
- **Risk:** Unrestricted database access can lead to data breaches and unauthorized data access.
### 4. iaas_security_group_all_traffic_unrestricted
- **Severity:** Critical
- **Description:** Detects security groups that allow all traffic from the internet.
- **Detection Logic:**
- Checks for rules with `port_range=None` (all ports)
- Checks for rules with port range covering 0-65535 or 1-65535
- Flags unrestricted IP ranges
- Critical security misconfiguration requiring immediate remediation
### Important Implementation Notes
**Self-Referencing Security Group Rules:**
Security group rules with `remoteSecurityGroupId` set are automatically filtered out from unrestricted access checks. These rules only allow traffic from instances within the same security group (self-referencing), not from the internet, and are therefore not flagged as security risks.
**Rule Display Names:**
All findings include user-friendly rule descriptions when available. If a security group rule has a description field set (the name shown in the StackIT UI), it will be displayed in the finding message along with the rule ID:
- With description: `'Allow SSH from office' (sgr-abc123)`
- Without description: `'sgr-abc123'`
**Network Interface (NIC) Usage Filtering:**
The IaaS service lists project NICs and records the security group IDs attached to them. Checks use that signal to decide whether a security group is in use:
1. **Default behavior:** Report security groups attached to at least one NIC.
2. **`--scan-unused-services`:** Report every security group, including unused ones.
3. **FAIL logic:** Internet exposure is driven by security group rules that allow unrestricted source ranges, not by the presence of a public IP on the NIC.
**Unrestricted IP Ranges:**
The StackIT API represents "unrestricted" in two ways:
- **`ip_range=null`**: No IP restriction specified (implicit unrestricted)
- **`ip_range="0.0.0.0/0"` or `"::/0"`**: Explicitly configured to allow all IPs
Both are flagged as unrestricted. A `null` value is **more permissive** than an explicit range and applies to all protocols/ports if other fields are also `null`.
## Requirements
### Python Version
- **Minimum:** Python 3.10+
- **Reason:** The StackIT SDK requires Python 3.10 or higher
### Dependencies
The StackIT provider requires the following Python packages (automatically installed with Prowler):
- **stackit-core** (v0.2.0): Core SDK for StackIT API authentication and configuration
- **stackit-iaas** (v1.4.0): IaaS service SDK for managing compute resources
- **stackit-resourcemanager** (v0.8.0): Resource Manager SDK for fetching project metadata (e.g., project names)
These dependencies are defined in `pyproject.toml` and installed automatically with:
```bash
poetry install
```
**Note:** The `stackit-resourcemanager` package enables automatic retrieval of project names for display in reports. If this package is not available, Prowler will still function normally but project names will be empty in the output.
## Region Support
### Supported Regions
- **Available Regions:** `eu01` (Germany South) and `eu02` (Austria West)
- **Default:** All scans use both `eu01` and `eu02` regions by default.
### Multi-Region Scanning
Prowler supports scanning multiple StackIT regions in a single execution. By default, it will scan all regions defined in the `stackit_regions_by_service.json` configuration file.
### CLI Argument
You can specify which regions to scan using the `--stackit-region` argument:
```bash
# Scan only eu01
prowler stackit --stackit-region eu01
# Scan both eu01 and eu02
prowler stackit --stackit-region eu01 eu02
```
### Implementation Details
- **Regional Clients:** Prowler generates a separate API client for each audited region.
- **Service Iteration:** Each service (e.g., IaaS) iterates through the regional clients to fetch and audit resources.
- **Identity Tracking:** The `audited_regions` are stored in the identity model for reporting.
### Future Enhancements
As StackIT adds more regions, they can be easily added to Prowler by updating the `prowler/providers/stackit/stackit_regions_by_service.json` file without requiring code changes.
## Command Examples
### Scan Specific Regions
Scan only the `eu01` region:
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--stackit-region eu01
```
Scan multiple regions:
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--stackit-region eu01 eu02
```
### Scan Specific Checks
Run only SSH unrestricted check:
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--checks iaas_security_group_ssh_unrestricted
```
### Scan All Security Group Checks
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--services iaas
```
### Output Formats
Generate JSON output:
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--output-formats json
```
Generate HTML report:
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--output-formats html
```
## Known Limitations
### Current Limitations
1. **Single Project Scope**: Only one project can be scanned at a time
2. **Service Coverage**: Only the IaaS service is currently implemented
3. **Check Coverage**: Limited to security group network security checks (4 checks total)
4. **No Compliance Frameworks**: Compliance framework mappings are not yet implemented
### Planned Enhancements
- Multi-project scanning capability
- Additional IaaS checks (volume encryption, server public IP exposure, backup status)
- Compliance framework mappings (CIS, custom StackIT best practices)
- StackIT CLI remediation examples in metadata
## Troubleshooting
### Authentication Errors
**Error:** `StackIT service account key was rejected`
**Solutions:**
1. Re-issue the service account key in the StackIT Portal
2. Verify the service account key file or inline JSON content is complete
3. Check that the service account has the necessary permissions (`iaas.viewer` or `project.owner`)
4. Ensure the service account key is provided through `STACKIT_SERVICE_ACCOUNT_KEY_PATH`, `STACKIT_SERVICE_ACCOUNT_KEY`, or the matching CLI arguments
**Error:** `StackIT credentials not found or are invalid`
**Solutions:**
1. Ensure the project ID and one service account credential source are provided
2. Check that credentials are set via environment variables or command-line arguments
3. Verify there are no extra spaces or newlines in the credentials
**Error:** `Invalid StackIT project ID format`
**Solutions:**
1. Verify the project ID is a valid UUID format: `12345678-1234-1234-1234-123456789abc`
2. Copy the project ID directly from the StackIT Portal
3. Ensure there are no extra spaces or quotes around the UUID
### API Connection Errors
**Error:** `Failed to connect to StackIT API`
**Solutions:**
1. Check your internet connection
2. Verify the StackIT API endpoint is accessible from your network
3. Check if there are any firewall rules blocking HTTPS connections
4. Review the full error message for specific API error codes
**Error:** `HTTP 403 Forbidden`
**Solutions:**
1. Verify the service account has the correct permissions
2. Ensure the project ID is correct and you have access to it
3. Check that the service account is enabled (not disabled or expired)
4. Verify the service account key has not been revoked
**Error:** `HTTP 404 Not Found`
**Solutions:**
1. Verify the project ID exists and is correct
2. Check that the IaaS service is enabled in your project
3. Ensure you're using the correct region (eu01)
### Empty Results
**Issue:** No security groups or findings reported
**Solutions:**
1. Verify that security groups exist in your project
2. Check that the IaaS service is properly configured
3. Ensure the service account has `iaas.viewer` permission
4. Check Prowler logs for any API errors (use `--log-level DEBUG`)
### Debug Mode
Enable debug logging for detailed troubleshooting:
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
prowler stackit \
--stackit-project-id "your-project-id" \
--log-level DEBUG
```
This will show:
- API authentication details (with inline service account keys redacted)
- Resource discovery progress
- Security rule parsing details
- Any API errors or warnings
## Specific Patterns in StackIT Services
The generic service pattern is described in [service page](/developer-guide/services#service-structure-and-initialisation). You can find all the currently implemented services in the following locations:
- Directly in the code, in location [`prowler/providers/stackit/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/stackit/services)
- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other StackIT services as reference.
### StackIT Service Common Patterns
- Services communicate with StackIT using the StackIT Python SDK, you can find the documentation [here](https://github.com/stackitcloud/stackit-sdk-python).
- Service constructors receive a `StackitProvider` instance and use it to access credentials, identity, and configuration.
- The provider builds StackIT SDK `Configuration` objects from the service account key path or inline key content.
- Resource containers **must** be initialized in the constructor, typically as lists or dictionaries.
- Do not manipulate `os.environ` for credentials inside services. Use the provider session and SDK configuration helpers.
- All StackIT resources are represented as Pydantic `BaseModel` classes, providing type safety and structured access to resource attributes.
- StackIT SDK calls are wrapped in try/except blocks, with specific handling for API errors, always logging errors.
- **Centralized Error Handling**: Use `provider.handle_api_error(exception)` for consistent authentication error detection across all services.
- **SDK Warning Suppression**: StackIT SDK prints deprecation warnings to stderr - use the `suppress_stderr()` context manager during SDK initialization and API calls.
- **Unrestricted Access Detection**: In StackIT API, `None` values mean "allow all" (more permissive than explicit 0.0.0.0/0).
- `protocol=None` → All protocols allowed
- `ip_range=None` → All source IPs allowed (unrestricted!)
- `port_range=None` → All ports allowed
- `remote_security_group_id` set → Only allows traffic from the same security group (not unrestricted!)
### IaaS Service Specific Patterns
**Security Group Discovery:**
```python
# List all security groups
security_groups = client.list_security_groups(
project_id=self.project_id,
region=region,
)
# List network interfaces to determine security group usage
nics = client.list_project_nics(
project_id=self.project_id,
region=region,
)
# Checks report in-use security groups by default. Use --scan-unused-services
# to include security groups that are not attached to any NIC.
```
**Centralized Authentication Error Handling:**
```python
def _handle_api_call(self, api_function, *args, **kwargs):
"""Wrapper for API calls with centralized error handling."""
try:
with suppress_stderr(): # Suppress SDK warnings
return api_function(*args, **kwargs)
except Exception as e:
# Use centralized error handler from provider
self.provider.handle_api_error(e) # Detects 401 and raises StackITInvalidTokenError
```
**Unrestricted Access Detection:**
```python
def is_unrestricted(rule):
"""Check if a rule allows unrestricted access."""
# Filter out self-referencing rules
if rule.remote_security_group_id is not None:
return False
# Check for unrestricted IP ranges
return rule.ip_range is None or rule.ip_range in ["0.0.0.0/0", "::/0"]
def is_tcp(rule):
"""Check if a rule applies to TCP protocol."""
# None means all protocols (including TCP)
return rule.protocol is None or rule.protocol.lower() in ["tcp", "all"]
def includes_port(rule, port):
"""Check if a rule includes a specific port."""
# None means all ports
if rule.port_range is None:
return True
return rule.port_range.min <= port <= rule.port_range.max
```
## Specific Patterns in StackIT Checks
The StackIT checks pattern is described in [checks page](/developer-guide/checks). You can find all the currently implemented checks:
- Directly in the code, within each service folder, each check has its own folder named after the name of the check. (e.g. [`prowler/providers/stackit/services/iaas/iaas_security_group_ssh_unrestricted/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/stackit/services/iaas/iaas_security_group_ssh_unrestricted))
- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
The best reference to understand how to implement a new check is following the [check creation documentation](/developer-guide/checks#creating-a-check) and taking other similar StackIT checks as reference.
### Check Report Class
The `CheckReportStackIT` class models a single finding for a StackIT resource in a check report. It is defined in [`prowler/lib/check/models.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py) and inherits from the generic `Check_Report` base class.
#### Purpose
`CheckReportStackIT` extends the base report structure with StackIT-specific fields, enabling detailed tracking of the resource, project, and location associated with each finding.
#### Constructor and Attribute Population
When you instantiate `CheckReportStackIT`, you must provide the check metadata and a resource object. The class will attempt to automatically populate its StackIT-specific attributes from the resource, using the following logic:
- **`resource_id`**:
- Uses `resource.id` if present.
- Otherwise, uses `resource.resource_id` if present.
- Defaults to an empty string if none are available.
- **`resource_name`**:
- Uses `resource.name` if present.
- Defaults to an empty string if not available.
- **`project_id`**:
- Uses `resource.project_id` if present.
- Defaults to an empty string if not available (should be set in check logic).
- **`location`**:
- Uses `resource.region` if present.
- Otherwise, uses `resource.location` if present.
- Defaults to an empty string if not available.
If the resource object does not contain the required attributes, you must set them manually in the check logic.
Other attributes are inherited from the `Check_Report` class, from which you **always** have to set the `status` and `status_extended` attributes in the check logic.
#### Example Usage
```python
from prowler.lib.check.models import CheckReportStackIT
report = CheckReportStackIT(
metadata=self.metadata(),
resource=security_group
)
report.status = "FAIL"
report.status_extended = f"Security group {security_group.name} allows unrestricted SSH access from the internet."
report.resource_id = security_group.id
report.resource_name = security_group.name
report.project_id = security_group.project_id
report.location = security_group.region
```
### Common Check Pattern
```python
from prowler.lib.check.models import Check, CheckReportStackIT
from prowler.providers.stackit.services.iaas.iaas_client import iaas_client
class iaas_security_group_ssh_unrestricted(Check):
"""Check if IaaS security groups allow unrestricted SSH access."""
def execute(self):
findings = []
for security_group in iaas_client.security_groups:
if not (iaas_client.scan_unused_services or security_group.in_use):
continue
report = CheckReportStackIT(
metadata=self.metadata(),
resource=security_group
)
report.status = "PASS"
report.status_extended = f"Security group {security_group.name} does not allow unrestricted SSH access."
# Check each rule
for rule in security_group.rules:
if (rule.is_ingress() and
rule.is_tcp() and
rule.includes_port(22) and
rule.is_unrestricted()):
report.status = "FAIL"
report.status_extended = f"Security group {security_group.name} allows unrestricted SSH access from the internet."
break
findings.append(report)
return findings
```
## Resources
### Official StackIT Documentation
- **StackIT Portal**: [https://portal.stackit.cloud/](https://portal.stackit.cloud/)
- **StackIT Documentation**: [https://docs.stackit.cloud/](https://docs.stackit.cloud/)
- **StackIT API Documentation**: [https://docs.api.eu01.stackit.cloud/](https://docs.api.eu01.stackit.cloud/)
### Python SDK
- **StackIT Python SDK (GitHub)**: [https://github.com/stackitcloud/stackit-sdk-python](https://github.com/stackitcloud/stackit-sdk-python)
- **stackit-core (PyPI)**: [https://pypi.org/project/stackit-core/](https://pypi.org/project/stackit-core/)
- **stackit-iaas (PyPI)**: [https://pypi.org/project/stackit-iaas/](https://pypi.org/project/stackit-iaas/)
- **IaaS Models**: [https://github.com/stackitcloud/stackit-sdk-python/tree/main/services/iaas/src/stackit/iaas/models](https://github.com/stackitcloud/stackit-sdk-python/tree/main/services/iaas/src/stackit/iaas/models)
### Prowler Resources
- **Provider Implementation**: [`prowler/providers/stackit/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/stackit/)
- **IaaS Service**: [`prowler/providers/stackit/services/iaas/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/stackit/services/iaas/)
- **Prowler Hub**: [https://hub.prowler.com/](https://hub.prowler.com/)
- **GitHub Issues**: [https://github.com/prowler-cloud/prowler/issues](https://github.com/prowler-cloud/prowler/issues)
## Contributing
If you'd like to contribute to the StackIT provider:
1. **Add New Checks**: Follow the [check creation guide](/developer-guide/checks#creating-a-check) and use existing StackIT checks as templates
2. **Enhance Services**: Implement additional IaaS resource discovery or add new services
3. **Improve Documentation**: Add metadata enhancements, CLI remediation examples, or Terraform code samples
4. **Report Issues**: Submit bug reports or feature requests on [GitHub](https://github.com/prowler-cloud/prowler/issues)
### Quick Start for Contributors
1. **Install dependencies**: `poetry install` (includes stackit-core and stackit-iaas)
2. **Set credentials**: Export `STACKIT_SERVICE_ACCOUNT_KEY_PATH` and `STACKIT_PROJECT_ID`
3. **Run checks**: `prowler stackit`
4. **View code**: Start in `prowler/providers/stackit/`
5. **Add checks**: Create new check directories under `services/iaas/`
6. **Run tests**: `poetry run pytest tests/providers/stackit/ -v`
### Code Quality Standards
The StackIT provider should follow the same quality expectations as the rest of the Prowler SDK:
- Keep service and check logic covered by unit tests.
- Redact inline service account keys from generated output.
- Keep documentation aligned with the implemented services and checks.
- Follow existing provider, service, and check patterns before adding StackIT-specific abstractions.
+19 -3
View File
@@ -124,8 +124,8 @@
"user-guide/tutorials/prowler-app-rbac",
"user-guide/tutorials/prowler-app-multi-tenant",
"user-guide/tutorials/prowler-app-api-keys",
"user-guide/tutorials/prowler-app-import-findings",
"user-guide/tutorials/prowler-app-alerts",
"user-guide/tutorials/prowler-import-findings",
"user-guide/tutorials/prowler-alerts",
{
"group": "Mutelist",
"expanded": true,
@@ -339,6 +339,13 @@
"user-guide/providers/scaleway/authentication"
]
},
{
"group": "StackIT",
"pages": [
"user-guide/providers/stackit/getting-started-stackit",
"user-guide/providers/stackit/authentication"
]
},
{
"group": "Vercel",
"pages": [
@@ -401,7 +408,8 @@
"developer-guide/kubernetes-details",
"developer-guide/m365-details",
"developer-guide/github-details",
"developer-guide/llm-details"
"developer-guide/llm-details",
"developer-guide/stackit-details"
]
},
{
@@ -576,6 +584,14 @@
{
"source": "/contact",
"destination": "/support"
},
{
"source": "/user-guide/tutorials/prowler-app-import-findings",
"destination": "/user-guide/tutorials/prowler-import-findings"
},
{
"source": "/user-guide/tutorials/prowler-app-alerts",
"destination": "/user-guide/tutorials/prowler-alerts"
}
]
}
+1
View File
@@ -36,6 +36,7 @@ Prowler supports a wide range of providers organized by category:
| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI |
| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI |
| [Scaleway](/user-guide/providers/scaleway/getting-started-scaleway) | [Contact us](https://prowler.com/contact) | Organizations | CLI |
| [StackIT](/user-guide/providers/stackit/getting-started-stackit) | [Contact us](https://prowler.com/contact) | Projects | CLI |
### Infrastructure as Code Providers
+3 -3
View File
@@ -6,7 +6,7 @@ title: 'Run Prowler in CI/CD and Send Findings to Prowler Cloud'
For new projects, use the official [Prowler GitHub Action](/user-guide/tutorials/prowler-app-github-action) — a Docker-based reusable action that runs scans, optionally pushes findings to Prowler Cloud, and uploads SARIF results to GitHub Code Scanning. The GitHub Actions examples below document the legacy pip-based flow.
</Warning>
This cookbook demonstrates how to integrate Prowler into CI/CD pipelines so that security scans run automatically and findings are sent to Prowler Cloud via [Import Findings](/user-guide/tutorials/prowler-app-import-findings). Examples cover GitHub Actions and GitLab CI.
This cookbook demonstrates how to integrate Prowler into CI/CD pipelines so that security scans run automatically and findings are sent to Prowler Cloud via [Import Findings](/user-guide/tutorials/prowler-import-findings). Examples cover GitHub Actions and GitLab CI.
## Prerequisites
@@ -19,7 +19,7 @@ This cookbook demonstrates how to integrate Prowler into CI/CD pipelines so that
Prowler CLI provides the `--push-to-cloud` flag, which uploads scan results directly to Prowler Cloud after a scan completes. Combined with the `PROWLER_CLOUD_API_KEY` environment variable, this enables fully automated ingestion without manual file uploads.
For full details on the flag and API, refer to the [Import Findings](/user-guide/tutorials/prowler-app-import-findings) documentation.
For full details on the flag and API, refer to the [Import Findings](/user-guide/tutorials/prowler-import-findings) documentation.
<Note>
The examples in this guide use AWS as the target provider, but the same approach applies to any provider supported by Prowler (Azure, GCP, Kubernetes, and others). Replace `prowler aws` with the desired provider command (e.g., `prowler gcp`, `prowler azure`) and configure the corresponding credentials in the CI/CD environment.
@@ -195,7 +195,7 @@ By default, Prowler exits with a non-zero code when it finds failing checks. Thi
* **GitLab CI**: Add `allow_failure: true` to the job
<Note>
Ingestion failures (e.g., network issues reaching Prowler Cloud) do not affect the Prowler exit code. The scan completes normally and only a warning is emitted. See [Import Findings troubleshooting](/user-guide/tutorials/prowler-app-import-findings#troubleshooting) for details.
Ingestion failures (e.g., network issues reaching Prowler Cloud) do not affect the Prowler exit code. The scan completes normally and only a warning is emitted. See [Import Findings troubleshooting](/user-guide/tutorials/prowler-import-findings#troubleshooting) for details.
</Note>
### Caching Prowler Installation
@@ -2,7 +2,7 @@
title: 'Run Kubernetes In-Cluster and Send Findings to Prowler Cloud'
---
This cookbook walks through deploying Prowler inside a Kubernetes cluster on a recurring schedule and automatically sending findings to Prowler Cloud via [Import Findings](/user-guide/tutorials/prowler-app-import-findings). By the end, security scan results from the cluster appear in Prowler Cloud without any manual file uploads.
This cookbook walks through deploying Prowler inside a Kubernetes cluster on a recurring schedule and automatically sending findings to Prowler Cloud via [Import Findings](/user-guide/tutorials/prowler-import-findings). By the end, security scan results from the cluster appear in Prowler Cloud without any manual file uploads.
## Prerequisites
@@ -181,7 +181,7 @@ Once the job completes and findings are pushed:
2. Open the "Scans" section to verify the ingestion job status
3. Browse findings under the Kubernetes provider
For details on the ingestion workflow and status tracking, refer to the [Import Findings](/user-guide/tutorials/prowler-app-import-findings) documentation.
For details on the ingestion workflow and status tracking, refer to the [Import Findings](/user-guide/tutorials/prowler-import-findings) documentation.
## Tips and Troubleshooting
@@ -204,4 +204,4 @@ For details on the ingestion workflow and status tracking, refer to the [Import
--namespace prowler-ns
```
* **Failed uploads**: If the push to Prowler Cloud fails, the scan still completes and findings are saved locally in the container. Check the [Import Findings troubleshooting section](/user-guide/tutorials/prowler-app-import-findings#troubleshooting) for common error messages.
* **Failed uploads**: If the push to Prowler Cloud fails, the scan still completes and findings are saved locally in the container. Check the [Import Findings troubleshooting section](/user-guide/tutorials/prowler-import-findings#troubleshooting) for common error messages.
@@ -18,7 +18,7 @@ Prowler requests the following read-only OAuth 2.0 scopes:
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
| `https://www.googleapis.com/auth/admin.directory.orgunit.readonly` | Read access to organizational unit hierarchy (identifies the root OU for policy filtering) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Gmail, Chat, and Drive service checks) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Chat, Drive, Gmail, Groups, Marketplace, Security, and Sites service checks) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
<Warning>
@@ -40,7 +40,7 @@ In the [Google Cloud Console](https://console.cloud.google.com), select the targ
| API | Required For |
|-----|--------------|
| **Admin SDK API** | Directory service checks (users, roles, domains) |
| **Cloud Identity API** | Calendar, Gmail, Chat, and Drive service checks (domain-level application policies) |
| **Cloud Identity API** | All service checks except Directory (domain-level application policies) |
For each API:
@@ -49,7 +49,7 @@ For each API:
3. Click **Enable**
<Note>
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar, Gmail, Chat, and Drive checks will return no findings if the Cloud Identity API is not enabled.
Both APIs must be enabled in the same GCP project that hosts the Service Account. All service checks except Directory will return no findings if the Cloud Identity API is not enabled.
</Note>
### Step 3: Create a Service Account
@@ -178,7 +178,7 @@ If Prowler connects but returns empty results or permission errors for specific
### Policy API Checks Return No Findings
If the Directory checks run successfully but the Calendar, Gmail, Chat, or Drive checks return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
If the Directory checks run successfully but other service checks (Calendar, Chat, Drive, Gmail, Groups, Marketplace, Security, Sites) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
- The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2)
- The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5)
@@ -0,0 +1,100 @@
---
title: 'StackIT Authentication'
---
Prowler authenticates with StackIT using a **service account key file**. The StackIT SDK signs the RSA challenge in the key file and mints/refreshes access tokens internally for the life of the scan, so no manual token rotation is needed.
## Service Account Key
StackIT uses RSA key-pair based service account keys. They are issued once, must be stored securely, and are read by the SDK on every scan to mint short-lived access tokens transparently.
### Option 1: Create the Key via the StackIT Portal
1. Open the [StackIT Portal](https://portal.stackit.cloud/) and select your project.
2. In the left sidebar, click **Service Accounts**.
3. Create a service account if you do not have one already. Assign:
- `iaas.viewer` for the IaaS security group checks currently shipped, or
- `project.owner` if you want to cover any future service Prowler adds.
4. Open the service account and go to **Service Account Keys**.
5. Click **Create key** and choose **STACKIT-generated key pair** (recommended). Download the resulting JSON file and store it securely (for example, `~/.stackit/sa-key.json`). The private material is only shown once.
### Option 2: Create the Key via the StackIT CLI
```bash
# Install the StackIT CLI from https://github.com/stackitcloud/stackit-cli first
stackit service-account key create --email my-service-account@example.com
```
## Project ID
Your StackIT project ID is a UUID. You can find it in:
1. The portal URL when viewing the project: `https://portal.stackit.cloud/projects/{PROJECT_ID}/...`
2. The project settings page
3. `stackit project list`
## Passing Credentials to Prowler
You can give Prowler either the **path** to the key file on disk or the **inline JSON content** of the key. Both go through the same StackIT SDK flow and refresh access tokens internally.
### Option A: Key File Path (workstation, persistent agents)
Recommended when the key is stored on disk.
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
export STACKIT_PROJECT_ID="12345678-1234-1234-1234-123456789abc"
prowler stackit
```
Or as CLI flags:
```bash
prowler stackit \
--stackit-service-account-key-path ~/.stackit/sa-key.json \
--stackit-project-id 12345678-1234-1234-1234-123456789abc
```
<Note>
Keep the key file outside of source control and lock it down with `chmod 600 ~/.stackit/sa-key.json`. Anyone with the JSON can mint access tokens for the service account.
</Note>
### Option B: Inline Key Content (CI/CD, secret managers)
Recommended when the key is fetched at run time from a secret manager (GitHub Actions secret, AWS Secrets Manager, HashiCorp Vault, etc.) and you do not want to write it to disk.
```bash
export STACKIT_SERVICE_ACCOUNT_KEY="$(vault kv get -field=key stackit/sa)"
export STACKIT_PROJECT_ID="12345678-1234-1234-1234-123456789abc"
prowler stackit
```
<Note>
Prefer the `STACKIT_SERVICE_ACCOUNT_KEY` environment variable over the matching CLI flag (`--stackit-service-account-key`); passing the secret on the command line leaks it through process listings and shell history.
</Note>
When both the inline content and a key path are set, the inline content wins.
## Credential Lookup Order
Prowler resolves credentials in this order:
1. CLI arguments: `--stackit-service-account-key`, `--stackit-service-account-key-path`, `--stackit-project-id`
2. Environment variables: `STACKIT_SERVICE_ACCOUNT_KEY`, `STACKIT_SERVICE_ACCOUNT_KEY_PATH`, `STACKIT_PROJECT_ID`
When both the inline key and the key file path are set, the inline content takes precedence.
## Token Lifetime
Access tokens are minted on demand by the SDK from the key file and refreshed before they expire. There is nothing to rotate while Prowler is running.
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---------|--------------|-----|
| `401 Unauthorized` during scan | Key file is missing fields, the public key is no longer registered, or the key was revoked | Re-issue the service account key in the StackIT portal and update `STACKIT_SERVICE_ACCOUNT_KEY_PATH` |
| `403 Forbidden` during scan | Service account lacks role on the project | Re-check role assignment in the StackIT portal; `iaas.viewer` is the minimum for the shipped IaaS checks |
| `StackIT project ID must be a valid UUID` | The project ID is not in UUID format | Copy the UUID from the portal URL or `stackit project list` |
| `StackIT service account credentials are required` | None of the four credential inputs is set | Export `STACKIT_SERVICE_ACCOUNT_KEY_PATH` or `STACKIT_SERVICE_ACCOUNT_KEY` (or use their CLI counterparts) before running Prowler |
@@ -0,0 +1,141 @@
---
title: 'Getting Started With StackIT'
---
Prowler supports [StackIT](https://www.stackit.de/) from the CLI. This guide walks you through the requirements and how to run scans.
<Note>
StackIT support in Prowler is community-maintained. For commercial support or to request additional service coverage, [contact us](https://prowler.com/contact).
</Note>
## Prerequisites
Before running Prowler with the StackIT provider, ensure you have:
1. A StackIT account with at least one project
2. A StackIT service account key file with permissions on the project (`iaas.viewer` is enough for the currently shipped IaaS checks; `project.owner` works for any future service). See the [Authentication guide](/user-guide/providers/stackit/authentication) for the full setup.
3. Access to Prowler CLI (see [Installation](/getting-started/installation/prowler-cli))
## Prowler CLI
### Step 1: Point Prowler at the Service Account Key
Prowler authenticates with a StackIT service account key. The SDK signs the RSA challenge in the key and refreshes access tokens internally for the life of the scan, so there is no manual token rotation.
**On a workstation or persistent agent** (key on disk):
```bash
export STACKIT_SERVICE_ACCOUNT_KEY_PATH="$HOME/.stackit/sa-key.json"
export STACKIT_PROJECT_ID="12345678-1234-1234-1234-123456789abc"
```
**In CI/CD** (key in a secret manager, never written to disk):
```bash
export STACKIT_SERVICE_ACCOUNT_KEY="$(vault kv get -field=key stackit/sa)"
export STACKIT_PROJECT_ID="12345678-1234-1234-1234-123456789abc"
```
CLI flags work too:
```bash
prowler stackit \
--stackit-service-account-key-path ~/.stackit/sa-key.json \
--stackit-project-id 12345678-1234-1234-1234-123456789abc
```
<Note>
For the inline key, prefer the `STACKIT_SERVICE_ACCOUNT_KEY` env var over the matching CLI flag; passing the secret on the command line leaks it through process listings and shell history.
Keep the key file outside of source control and lock it down with `chmod 600 ~/.stackit/sa-key.json`. Anyone with the JSON can mint access tokens for the service account.
</Note>
### Step 2: Run Your First Scan
```bash
prowler stackit
```
Prowler will discover and audit the project's IaaS security groups across the available StackIT regions.
**Scan specific regions:**
```bash
prowler stackit --stackit-region eu01 eu02
```
**Run specific security checks:**
```bash
prowler stackit --checks iaas_security_group_ssh_unrestricted
# List all available checks
prowler stackit --list-checks
```
**Filter by check severity:**
```bash
prowler stackit --severity critical high
```
**Generate specific output formats:**
```bash
# JSON only
prowler stackit --output-modes json
# CSV and HTML
prowler stackit --output-modes csv html
# Custom output directory
prowler stackit --output-directory /path/to/reports/
```
**Use a mutelist to suppress findings:**
```yaml
# mutelist.yaml
Mutelist:
Accounts:
"12345678-1234-1234-1234-123456789abc":
Checks:
iaas_security_group_ssh_unrestricted:
Regions:
- "*"
Resources:
- "test-sg-id"
Tags: []
```
```bash
prowler stackit --mutelist-file mutelist.yaml
```
### Step 3: Review the Results
Prowler outputs findings to the console and writes reports to the `output/` directory by default:
- CSV: `output/prowler-output-stackit-{project_id}-{timestamp}.csv`
- JSON: `output/prowler-output-stackit-{project_id}-{timestamp}.json`
- HTML: `output/prowler-output-stackit-{project_id}-{timestamp}.html`
## Supported StackIT Services
| Service | StackIT API | Description | Example Checks |
|---------|-------------|-------------|----------------|
| **IaaS** | `iaas` | Virtual machines, network interfaces, security groups | `iaas_security_group_ssh_unrestricted`, `iaas_security_group_rdp_unrestricted`, `iaas_security_group_database_unrestricted`, `iaas_security_group_all_traffic_unrestricted` |
Additional services will be added in future releases. Track progress in the [Prowler release notes](https://github.com/prowler-cloud/prowler/releases).
## Troubleshooting
### Authentication Errors
If the scan fails with a 401 error, the service account key is no longer valid (revoked, rotated or the key file is incomplete). Re-issue the key in the [StackIT portal](https://portal.stackit.cloud/) and update `STACKIT_SERVICE_ACCOUNT_KEY_PATH`.
### Permission Errors
If checks fail with a 403 error, the service account is missing the required role on the project. Re-check the role assignment in the StackIT portal (`iaas.viewer` is the minimum for the shipped IaaS checks).
For detailed setup steps, see the [Authentication guide](/user-guide/providers/stackit/authentication).
@@ -10,7 +10,7 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
Alerts notify recipients by email when security findings match saved filter conditions. Use Alerts to track high-priority findings, monitor specific providers or services, and keep teams informed about scan results that match defined criteria.
<Note>
This feature is available exclusively in **Prowler Cloud** with a paid subscription.
This feature is available exclusively in **Prowler Cloud** and **Prowler Enterprise** with a [paid subscription](https://prowler.com/pricing).
</Note>
## Prerequisites
@@ -18,7 +18,7 @@ Source: [`prowler-cloud/prowler`](https://github.com/prowler-cloud/prowler) · M
| `provider` | yes | — | Cloud provider to scan (`aws`, `azure`, `gcp`, `github`, `kubernetes`, `iac`, `cloudflare`, etc.) |
| `image-tag` | no | `stable` | Docker image tag — `stable` (latest release), `latest` (master, not stable), or `<x.y.z>` (pinned). See [available tags](https://hub.docker.com/r/prowlercloud/prowler/tags). |
| `output-formats` | no | `json-ocsf` | Output format(s) for scan results. Space-separated (e.g. `sarif json-ocsf`) |
| `push-to-cloud` | no | `false` | Push findings to [Prowler Cloud](/user-guide/tutorials/prowler-app-import-findings). When `true`, `PROWLER_CLOUD_API_KEY` is auto-forwarded |
| `push-to-cloud` | no | `false` | Push findings to [Prowler Cloud](/user-guide/tutorials/prowler-import-findings). When `true`, `PROWLER_CLOUD_API_KEY` is auto-forwarded |
| `flags` | no | `""` | Additional CLI flags (e.g. `--severity critical high`). Values with spaces can be quoted: `--resource-tag 'Environment=My Server'` |
| `extra-env` | no | `""` | Space-, newline-, or comma-separated list of env var **names** to forward to the container (see [Authentication](#authentication)) |
| `upload-sarif` | no | `false` | Upload SARIF results to GitHub Code Scanning |
@@ -43,7 +43,7 @@ Source: [`prowler-cloud/prowler`](https://github.com/prowler-cloud/prowler) · M
### Push findings to Prowler Cloud
Send scan results directly to [Prowler Cloud](/user-guide/tutorials/prowler-app-import-findings) for centralized visibility, compliance tracking, and team collaboration.
Send scan results directly to [Prowler Cloud](/user-guide/tutorials/prowler-import-findings) for centralized visibility, compliance tracking, and team collaboration.
```yaml
- uses: prowler-cloud/prowler@5.25
@@ -239,7 +239,7 @@ To grant all administrative permissions, select the **Grant all admin permission
The following permissions are available exclusively in **Prowler Cloud**:
**Manage Ingestions:** Submit and manage findings ingestion jobs via the API. Required to upload OCSF scan results using the `--push-to-cloud` CLI flag or the ingestion endpoints. See [Import Findings](/user-guide/tutorials/prowler-app-import-findings) for details.
**Manage Ingestions:** Submit and manage findings ingestion jobs via the API. Required to upload OCSF scan results using the `--push-to-cloud` CLI flag or the ingestion endpoints. See [Import Findings](/user-guide/tutorials/prowler-import-findings) for details.
**Manage Billing:** Access and manage billing settings, subscription plans, and payment methods.
@@ -10,7 +10,7 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
Findings Ingestion enables uploading OCSF (Open Cybersecurity Schema Framework) scan results to Prowler Cloud. This feature supports importing findings from Prowler CLI output files that use the [Detection Finding](https://schema.ocsf.io/classes/detection_finding) class.
<Note>
This feature is available exclusively in **Prowler Cloud** with a paid subscription.
This feature is available exclusively in **Prowler Cloud** and **Prowler Enterprise** with a [paid subscription](https://prowler.com/pricing).
</Note>
## OCSF Detection Finding format
+11 -2
View File
@@ -2,7 +2,7 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.29.0] (Prowler UNRELEASED)
## [5.29.0] (Prowler v5.29.0)
### 🚀 Added
@@ -11,16 +11,25 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `storage_account_public_network_access_disabled` check for Azure provider and remapped the Azure CIS "Public Network Access is Disabled" requirements to it [(#11334)](https://github.com/prowler-cloud/prowler/pull/11334)
- Support for external/custom providers, checks, and compliance frameworks without modifying core code [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700)
- Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398)
- StackIT provider with service account key authentication [(#9237)](https://github.com/prowler-cloud/prowler/pull/9237)
- 8 Rules service checks for Google Workspace provider using the Cloud Identity Policy API [(#11379)](https://github.com/prowler-cloud/prowler/pull/11379)
- 12 Security service checks for Google Workspace provider using the Cloud Identity Policy API [(#11356)](https://github.com/prowler-cloud/prowler/pull/11356)
### ⚠️ Deprecated
- `s3_bucket_default_encryption` check for AWS provider since SSE-S3 is automatically applied to all S3 buckets by AWS as of January 5, 2023 and can no longer be disabled [(#11230)](https://github.com/prowler-cloud/prowler/pull/11230)
### 🐞 Fixed
- ENS RD 311/2022 (AWS) compliance mapping: `vpc_different_regions` was uncorrectly mapped under the `mp.com.4` family (Network segregation). That check is now mapped to a new `op.cont.2.aws.vpc.1` requirement under the Continuity of Service control [(#11372)](https://github.com/prowler-cloud/prowler/pull/11372)
- Compliance CSV row count now matches the UI per requirement by sourcing rows from the framework JSON's `requirement.Checks` instead of the stale `finding.compliance` snapshot [(#11370)](https://github.com/prowler-cloud/prowler/pull/11370)
- `load_and_validate_config_file` now unwraps namespaced config for every built-in and external provider, and no longer leaks the full file as the provider's config when the file is namespaced [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700)
- OpenStack provider exception codes moved from the `10000-10999` range, shared with the AlibabaCloud provider, to the free `17000-17999` range to keep error codes unambiguous [(#11382)](https://github.com/prowler-cloud/prowler/pull/11382)
- Azure provider authentication against sovereign clouds (`AzureChinaCloud`, `AzureUSGovernment`) [(#10284)](https://github.com/prowler-cloud/prowler/pull/10284)
---
## [5.28.1] (Prowler 5.28.1)
## [5.28.1] (Prowler v5.28.1)
### 🐞 Fixed
+5
View File
@@ -157,6 +157,7 @@ from prowler.providers.okta.models import OktaOutputOptions
from prowler.providers.openstack.models import OpenStackOutputOptions
from prowler.providers.oraclecloud.models import OCIOutputOptions
from prowler.providers.scaleway.models import ScalewayOutputOptions
from prowler.providers.stackit.models import StackITOutputOptions
from prowler.providers.vercel.models import VercelOutputOptions
@@ -416,6 +417,10 @@ def prowler():
output_options = OCIOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "stackit":
output_options = StackITOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "alibabacloud":
output_options = AlibabaCloudOutputOptions(
args, bulk_checks_metadata, global_provider.identity
@@ -1360,7 +1360,9 @@
{
"Id": "4.1.1.1",
"Description": "Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users in administrative roles",
"Checks": [],
"Checks": [
"security_2sv_enforced"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1381,7 +1383,9 @@
{
"Id": "4.1.1.2",
"Description": "Ensure hardware security keys are used for all users in administrative roles and other high-value accounts",
"Checks": [],
"Checks": [
"security_2sv_hardware_keys_admins"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1402,7 +1406,9 @@
{
"Id": "4.1.1.3",
"Description": "Ensure 2-Step Verification (Multi-Factor Authentication) is enforced for all users",
"Checks": [],
"Checks": [
"security_2sv_enforced"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1423,7 +1429,9 @@
{
"Id": "4.1.2.1",
"Description": "Ensure Super Admin account recovery is disabled",
"Checks": [],
"Checks": [
"security_super_admin_recovery_disabled"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1444,7 +1452,9 @@
{
"Id": "4.1.2.2",
"Description": "Ensure User account recovery is enabled",
"Checks": [],
"Checks": [
"security_user_recovery_enabled"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1465,7 +1475,9 @@
{
"Id": "4.1.3.1",
"Description": "Ensure Advanced Protection Program is configured",
"Checks": [],
"Checks": [
"security_advanced_protection_configured"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1486,7 +1498,9 @@
{
"Id": "4.1.4.1",
"Description": "Ensure login challenges are enforced",
"Checks": [],
"Checks": [
"security_login_challenges_configured"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1507,7 +1521,9 @@
{
"Id": "4.1.5.1",
"Description": "Ensure password policy is configured for enhanced security",
"Checks": [],
"Checks": [
"security_password_policy_strong"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1528,7 +1544,9 @@
{
"Id": "4.2.1.1",
"Description": "Ensure application access to Google services is restricted",
"Checks": [],
"Checks": [
"security_app_access_restricted"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1570,7 +1588,9 @@
{
"Id": "4.2.1.3",
"Description": "Ensure internal apps can access Google Workspace APIs",
"Checks": [],
"Checks": [
"security_internal_apps_trusted"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1633,7 +1653,9 @@
{
"Id": "4.2.3.1",
"Description": "Ensure DLP policies for Google Drive are configured",
"Checks": [],
"Checks": [
"security_dlp_drive_rules_configured"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1654,7 +1676,9 @@
{
"Id": "4.2.4.1",
"Description": "Ensure Google session control is configured",
"Checks": [],
"Checks": [
"security_session_duration_limited"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1696,7 +1720,9 @@
{
"Id": "4.2.6.1",
"Description": "Ensure less secure app access is disabled",
"Checks": [],
"Checks": [
"security_less_secure_apps_disabled"
],
"Attributes": [
{
"Section": "4 Security",
@@ -1801,7 +1827,9 @@
{
"Id": "6.1",
"Description": "Ensure User's password changed is configured",
"Checks": [],
"Checks": [
"rules_password_changed_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1822,7 +1850,9 @@
{
"Id": "6.2",
"Description": "Ensure Government-backed attacks is configured",
"Checks": [],
"Checks": [
"rules_government_backed_attacks_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1843,7 +1873,9 @@
{
"Id": "6.3",
"Description": "Ensure User suspended due to suspicious activity is configured",
"Checks": [],
"Checks": [
"rules_suspicious_activity_suspension_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1864,7 +1896,9 @@
{
"Id": "6.4",
"Description": "Ensure User granted Admin privilege is configured",
"Checks": [],
"Checks": [
"rules_admin_privilege_granted_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1885,7 +1919,9 @@
{
"Id": "6.5",
"Description": "Ensure Suspicious programmatic login is configured",
"Checks": [],
"Checks": [
"rules_suspicious_programmatic_login_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1906,7 +1942,9 @@
{
"Id": "6.6",
"Description": "Ensure Suspicious login is configured",
"Checks": [],
"Checks": [
"rules_suspicious_login_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1927,7 +1965,9 @@
{
"Id": "6.7",
"Description": "Ensure Leaked password is configured",
"Checks": [],
"Checks": [
"rules_leaked_password_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -1948,7 +1988,9 @@
{
"Id": "6.8",
"Description": "Ensure Gmail potential employee spoofing is configured",
"Checks": [],
"Checks": [
"rules_gmail_employee_spoofing_alert_configured"
],
"Attributes": [
{
"Section": "6 Rules",
@@ -8,7 +8,10 @@
{
"Id": "GWS.COMMONCONTROLS.1.1",
"Description": "Phishing-resistant MFA SHALL be required for all users",
"Checks": [],
"Checks": [
"security_2sv_enforced",
"security_2sv_hardware_keys_admins"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -21,7 +24,9 @@
{
"Id": "GWS.COMMONCONTROLS.1.2",
"Description": "If phishing-resistant MFA is not yet tenable, an MFA method from the list of acceptable MFA methods SHALL be used as an interim solution",
"Checks": [],
"Checks": [
"security_2sv_enforced"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -112,7 +117,9 @@
{
"Id": "GWS.COMMONCONTROLS.4.1",
"Description": "Google Workspace sessions SHALL re-authenticate after 12 hours",
"Checks": [],
"Checks": [
"security_session_duration_limited"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -125,7 +132,9 @@
{
"Id": "GWS.COMMONCONTROLS.5.1",
"Description": "Password strength SHALL be enforced",
"Checks": [],
"Checks": [
"security_password_policy_strong"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -138,7 +147,9 @@
{
"Id": "GWS.COMMONCONTROLS.5.2",
"Description": "Minimum password length SHALL be at least 12 characters",
"Checks": [],
"Checks": [
"security_password_policy_strong"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -151,7 +162,9 @@
{
"Id": "GWS.COMMONCONTROLS.5.3",
"Description": "Minimum password length SHOULD be at least 15 characters",
"Checks": [],
"Checks": [
"security_password_policy_strong"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -164,7 +177,9 @@
{
"Id": "GWS.COMMONCONTROLS.5.4",
"Description": "Password policy SHALL be enforced at next sign-in",
"Checks": [],
"Checks": [
"security_password_policy_strong"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -177,7 +192,9 @@
{
"Id": "GWS.COMMONCONTROLS.5.5",
"Description": "Password reuse SHALL be restricted",
"Checks": [],
"Checks": [
"security_password_policy_strong"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -244,7 +261,9 @@
{
"Id": "GWS.COMMONCONTROLS.8.1",
"Description": "Account recovery for super admins SHALL be disabled",
"Checks": [],
"Checks": [
"security_super_admin_recovery_disabled"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -283,7 +302,9 @@
{
"Id": "GWS.COMMONCONTROLS.9.1",
"Description": "Privileged accounts SHALL be enrolled in the Advanced Protection Program",
"Checks": [],
"Checks": [
"security_advanced_protection_configured"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -296,7 +317,9 @@
{
"Id": "GWS.COMMONCONTROLS.9.2",
"Description": "Sensitive user accounts SHOULD be enrolled in the Advanced Protection Program",
"Checks": [],
"Checks": [
"security_advanced_protection_configured"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -361,7 +384,9 @@
{
"Id": "GWS.COMMONCONTROLS.10.5",
"Description": "Internal apps SHALL be allowed to access restricted Google Workspace APIs",
"Checks": [],
"Checks": [
"security_internal_apps_trusted"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -402,7 +427,16 @@
{
"Id": "GWS.COMMONCONTROLS.13.1",
"Description": "All system-defined alerting rules SHALL be enabled with alerts sent to admin email addresses",
"Checks": [],
"Checks": [
"rules_password_changed_alert_configured",
"rules_government_backed_attacks_alert_configured",
"rules_suspicious_activity_suspension_alert_configured",
"rules_admin_privilege_granted_alert_configured",
"rules_suspicious_programmatic_login_alert_configured",
"rules_suspicious_login_alert_configured",
"rules_leaked_password_alert_configured",
"rules_gmail_employee_spoofing_alert_configured"
],
"Attributes": [
{
"Section": "Common Controls",
@@ -506,7 +540,9 @@
{
"Id": "GWS.COMMONCONTROLS.18.1",
"Description": "A DLP policy SHALL be configured for Drive",
"Checks": [],
"Checks": [
"security_dlp_drive_rules_configured"
],
"Attributes": [
{
"Section": "Common Controls",
Whitespace-only changes.
+1
View File
@@ -79,6 +79,7 @@ class Provider(str, Enum):
SCALEWAY = "scaleway"
VERCEL = "vercel"
OKTA = "okta"
STACKIT = "stackit"
# Compliance
@@ -0,0 +1,26 @@
### Project, Check and/or Region can be * to apply for all the cases.
### Project == <StackIT Project ID>
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Projects, Regions, Resources and/or Tags.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
"project_id_1":
Checks:
"iaas_security_group_ssh_unrestricted":
Regions:
- "*"
Resources:
- "sg-production-ssh"
- "sg-development-rdp"
Tags:
- "environment=dev"
"project_id_2":
Checks:
"*":
Regions:
- "eu01"
Resources:
- ".*-test$"
+25
View File
@@ -1255,6 +1255,31 @@ class CheckReportNHN(Check_Report):
self.location = getattr(resource, "location", "kr1")
@dataclass
class CheckReportStackIT(Check_Report):
"""Contains the StackIT Check's finding information."""
resource_name: str
resource_id: str
project_id: str
location: str
def __init__(self, metadata: Dict, resource: Any) -> None:
"""Initialize the StackIT Check's finding information.
Args:
metadata: The metadata of the check.
resource: Basic information about the resource. Defaults to None.
"""
super().__init__(metadata, resource)
self.resource_name = getattr(
resource, "name", getattr(resource, "resource_name", "")
)
self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", ""))
self.project_id = getattr(resource, "project_id", "")
self.location = getattr(resource, "region", getattr(resource, "location", ""))
@dataclass
class CheckReportOpenStack(Check_Report):
"""Contains the OpenStack Check's finding information."""
+6 -2
View File
@@ -46,6 +46,9 @@ class ProwlerArgumentParser:
"nhn",
"mongodbatlas",
"vercel",
"okta",
"scaleway",
"stackit",
}
all_providers = set(Provider.get_available_providers())
new_providers = sorted(all_providers - known_providers)
@@ -68,10 +71,10 @@ class ProwlerArgumentParser:
self.parser = argparse.ArgumentParser(
prog="prowler",
formatter_class=RawTextHelpFormatter,
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,vercel,dashboard,iac,image,llm{extra_providers_csv}}} ...",
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,dashboard,iac,image,llm{extra_providers_csv}}} ...",
epilog=f"""
Available Cloud Providers:
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,vercel{extra_providers_csv}}}
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel{extra_providers_csv}}}
aws AWS Provider
azure Azure Provider
gcp GCP Provider
@@ -83,6 +86,7 @@ Available Cloud Providers:
cloudflare Cloudflare Provider
oraclecloud Oracle Cloud Infrastructure Provider
openstack OpenStack Provider
stackit StackIT Provider
alibabacloud Alibaba Cloud Provider
iac IaC Provider
llm LLM Provider (Beta)
+16
View File
@@ -342,6 +342,20 @@ class Finding(BaseModel):
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = check_output.location
elif provider.type == "stackit":
output_data["auth_method"] = getattr(
provider, "auth_method", "api_token"
)
output_data["account_uid"] = get_nested_attribute(
provider, "identity.project_id"
)
output_data["account_name"] = get_nested_attribute(
provider, "identity.project_name"
)
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = check_output.location
elif provider.type == "iac":
output_data["auth_method"] = provider.auth_method
provider_uid = getattr(provider, "provider_uid", None)
@@ -581,6 +595,8 @@ class Finding(BaseModel):
finding.subscription = list(provider.identity.subscriptions.keys())[0]
elif provider.type == "gcp":
finding.project_id = list(provider.projects.keys())[0]
elif provider.type == "stackit":
finding.project_id = provider.identity.project_id
elif provider.type == "iac":
# For IaC, we don't have resource_line_range in the Finding model
# It would need to be extracted from the resource metadata if needed
+67
View File
@@ -1076,6 +1076,73 @@ class HTML(Output):
)
return ""
@staticmethod
def get_stackit_assessment_summary(provider: Provider) -> str:
"""
get_stackit_assessment_summary gets the HTML assessment summary for the StackIT provider
Args:
provider (Provider): the StackIT provider object
Returns:
str: HTML assessment summary for the StackIT provider
"""
try:
project_id = getattr(provider.identity, "project_id", "unknown")
project_name = getattr(provider.identity, "project_name", "")
audited_regions = getattr(provider.identity, "audited_regions", set())
project_name_item = (
f"""
<li class="list-group-item">
<b>Project Name:</b> {project_name}
</li>"""
if project_name
else ""
)
regions_item = (
f"""
<li class="list-group-item">
<b>Regions:</b> {", ".join(sorted(audited_regions))}
</li>"""
if audited_regions
else ""
)
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
StackIT Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Project ID:</b> {project_id}
</li>
{project_name_item}
{regions_item}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
StackIT Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>Authentication Type:</b> Service Account Key
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_cloudflare_assessment_summary(provider: Provider) -> str:
"""
+2
View File
@@ -24,6 +24,8 @@ def stdout_report(finding, color, verbose, status, fix, provider=None):
details = finding.location
elif finding.check_metadata.Provider == "nhn":
details = finding.location
elif finding.check_metadata.Provider == "stackit":
details = finding.location
elif finding.check_metadata.Provider == "llm":
details = finding.check_metadata.CheckID
elif finding.check_metadata.Provider == "iac":
+7
View File
@@ -70,6 +70,13 @@ def display_summary_table(
elif provider.type == "nhn":
entity_type = "Tenant Domain"
audited_entities = provider.identity.tenant_domain
elif provider.type == "stackit":
if provider.identity.project_name:
entity_type = "Project"
audited_entities = provider.identity.project_name
else:
entity_type = "Project ID"
audited_entities = provider.identity.project_id
elif provider.type == "iac":
if provider.scan_repository_url:
entity_type = "Repository"
@@ -1,7 +1,7 @@
{
"Provider": "aws",
"CheckID": "s3_bucket_default_encryption",
"CheckTitle": "S3 bucket has default server-side encryption (SSE) enabled",
"CheckTitle": "[DEPRECATED] S3 bucket has default server-side encryption (SSE) enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
@@ -14,13 +14,11 @@
"Severity": "medium",
"ResourceType": "AwsS3Bucket",
"ResourceGroup": "storage",
"Description": "**Amazon S3 buckets** have a default **server-side encryption** setting that automatically encrypts new objects using `SSE-S3` or `SSE-KMS`. This evaluates whether a bucket has a default encryption configuration defined.",
"Description": "[DEPRECATED] **Amazon S3 buckets** have a default **server-side encryption** setting that automatically encrypts new objects using `SSE-S3` or `SSE-KMS`. This evaluates whether a bucket has a default encryption configuration defined.",
"Risk": "Without default encryption, older objects may remain unencrypted and new uploads won't be forced to use `SSE-KMS`. This reduces confidentiality and governance by limiting key audit logs, rotation, and cross-account controls, and increases exposure if data is copied, replicated, or accessed outside intended paths.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.amazonaws.cn/en_us/AmazonS3/latest/userguide/bucket-encryption.html",
"https://aws.amazon.com/blogs/security/how-to-prevent-uploads-of-unencrypted-objects-to-amazon-s3/",
"https://docs.aws.amazon.com/us_en/AmazonS3/latest/userguide/default-encryption-faq.html"
"https://docs.aws.amazon.com/AmazonS3/latest/userguide/default-encryption-faq.html"
],
"Remediation": {
"Code": {
@@ -39,5 +37,5 @@
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
"Notes": "This check is being deprecated since AWS automatically applies SSE-S3 to every S3 bucket (both new buckets and previously-unencrypted existing buckets) as of January 5, 2023, and encryption can no longer be disabled. For SSE-KMS validation, use `s3_bucket_kms_encryption` instead."
}
+50 -9
View File
@@ -241,7 +241,10 @@ class AzureProvider(Provider):
azure_credentials = None
if tenant_id and client_id and client_secret:
azure_credentials = self.validate_static_credentials(
tenant_id=tenant_id, client_id=client_id, client_secret=client_secret
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
region_config=self._region_config,
)
# Set up the Azure session
@@ -410,6 +413,9 @@ class AzureProvider(Provider):
authority=config["authority"],
base_url=config["base_url"],
credential_scopes=config["credential_scopes"],
graph_host=config["graph_host"],
graph_scope=config["graph_scope"],
logs_endpoint=config["logs_endpoint"],
)
except ArgumentTypeError as validation_error:
logger.error(
@@ -507,6 +513,7 @@ class AzureProvider(Provider):
tenant_id=azure_credentials["tenant_id"],
client_id=azure_credentials["client_id"],
client_secret=azure_credentials["client_secret"],
authority=region_config.authority,
)
return credentials
except ClientAuthenticationError as error:
@@ -579,7 +586,10 @@ class AzureProvider(Provider):
)
else:
try:
credentials = InteractiveBrowserCredential(tenant_id=tenant_id)
credentials = InteractiveBrowserCredential(
tenant_id=tenant_id,
authority=region_config.authority,
)
except Exception as error:
logger.critical(
"Failed to retrieve azure credentials using browser authentication"
@@ -662,6 +672,7 @@ class AzureProvider(Provider):
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
region_config=region_config,
)
# Set up the Azure session
@@ -675,7 +686,11 @@ class AzureProvider(Provider):
region_config,
)
# Create a SubscriptionClient
subscription_client = SubscriptionClient(credentials)
subscription_client = SubscriptionClient(
credentials,
base_url=region_config.base_url,
credential_scopes=region_config.credential_scopes,
)
# Get info from the subscriptions
available_subscriptions = []
@@ -1039,7 +1054,11 @@ class AzureProvider(Provider):
}
"""
credentials = self.session
subscription_client = SubscriptionClient(credentials)
subscription_client = SubscriptionClient(
credentials,
base_url=self.region_config.base_url,
credential_scopes=self.region_config.credential_scopes,
)
locations = {}
for subscription_id, display_name in self._identity.subscriptions.items():
@@ -1084,7 +1103,10 @@ class AzureProvider(Provider):
@staticmethod
def validate_static_credentials(
tenant_id: str = None, client_id: str = None, client_secret: str = None
tenant_id: str = None,
client_id: str = None,
client_secret: str = None,
region_config: AzureRegionConfig = None,
) -> dict:
"""
Validates the static credentials for the Azure provider.
@@ -1093,6 +1115,9 @@ class AzureProvider(Provider):
tenant_id (str): The Azure Active Directory tenant ID.
client_id (str): The Azure client ID.
client_secret (str): The Azure client secret.
region_config (AzureRegionConfig): The region configuration used to
build the per-cloud login endpoint and Graph scope. Defaults to
the public-cloud configuration when not provided.
Raises:
AzureNotValidTenantIdError: If the provided Azure Tenant ID is not valid.
@@ -1129,8 +1154,13 @@ class AzureProvider(Provider):
message="The provided Azure Client Secret is not valid.",
)
if region_config is None:
region_config = AzureProvider.setup_region_config("AzureCloud")
try:
AzureProvider.verify_client(tenant_id, client_id, client_secret)
AzureProvider.verify_client(
tenant_id, client_id, client_secret, region_config
)
return {
"tenant_id": tenant_id,
"client_id": client_id,
@@ -1162,7 +1192,9 @@ class AzureProvider(Provider):
)
@staticmethod
def verify_client(tenant_id, client_id, client_secret) -> None:
def verify_client(
tenant_id, client_id, client_secret, region_config: AzureRegionConfig = None
) -> None:
"""
Verifies the Azure client credentials using the specified tenant ID, client ID, and client secret.
@@ -1170,6 +1202,9 @@ class AzureProvider(Provider):
tenant_id (str): The Azure Active Directory tenant ID.
client_id (str): The Azure client ID.
client_secret (str): The Azure client secret.
region_config (AzureRegionConfig): The region configuration used to
build the per-cloud login endpoint and Graph scope. Defaults to
the public-cloud configuration when not provided.
Raises:
AzureNotValidTenantIdError: If the provided Azure Tenant ID is not valid.
@@ -1179,7 +1214,13 @@ class AzureProvider(Provider):
Returns:
None
"""
url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
if region_config is None:
region_config = AzureProvider.setup_region_config("AzureCloud")
# `authority` is None for the public cloud and a bare host (e.g.
# `login.chinacloudapi.cn`) for sovereign clouds, mirroring the
# `AzureAuthorityHosts` constants used by azure-identity.
login_endpoint = region_config.authority or "login.microsoftonline.com"
url = f"https://{login_endpoint}/{tenant_id}/oauth2/v2.0/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
@@ -1188,7 +1229,7 @@ class AzureProvider(Provider):
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://graph.microsoft.com/.default",
"scope": region_config.graph_scope,
}
response = requests.post(url, headers=headers, data=data).json()
if "access_token" not in response.keys() and "error_codes" in response.keys():
@@ -4,6 +4,18 @@ AZURE_CHINA_CLOUD = "https://management.chinacloudapi.cn"
AZURE_US_GOV_CLOUD = "https://management.usgovcloudapi.net"
AZURE_GENERIC_CLOUD = "https://management.azure.com"
AZURE_GENERIC_GRAPH_HOST = "https://graph.microsoft.com"
AZURE_CHINA_GRAPH_HOST = "https://microsoftgraph.chinacloudapi.cn"
AZURE_US_GOV_GRAPH_HOST = "https://graph.microsoft.us"
AZURE_GENERIC_GRAPH_SCOPE = f"{AZURE_GENERIC_GRAPH_HOST}/.default"
AZURE_CHINA_GRAPH_SCOPE = f"{AZURE_CHINA_GRAPH_HOST}/.default"
AZURE_US_GOV_GRAPH_SCOPE = f"{AZURE_US_GOV_GRAPH_HOST}/.default"
AZURE_GENERIC_LOGS_ENDPOINT = "https://api.loganalytics.io"
AZURE_CHINA_LOGS_ENDPOINT = "https://api.loganalytics.azure.cn"
AZURE_US_GOV_LOGS_ENDPOINT = "https://api.loganalytics.us"
def get_regions_config(region):
allowed_regions = {
@@ -11,16 +23,25 @@ def get_regions_config(region):
"authority": None,
"base_url": AZURE_GENERIC_CLOUD,
"credential_scopes": [AZURE_GENERIC_CLOUD + "/.default"],
"graph_host": AZURE_GENERIC_GRAPH_HOST,
"graph_scope": AZURE_GENERIC_GRAPH_SCOPE,
"logs_endpoint": AZURE_GENERIC_LOGS_ENDPOINT,
},
"AzureChinaCloud": {
"authority": AzureAuthorityHosts.AZURE_CHINA,
"base_url": AZURE_CHINA_CLOUD,
"credential_scopes": [AZURE_CHINA_CLOUD + "/.default"],
"graph_host": AZURE_CHINA_GRAPH_HOST,
"graph_scope": AZURE_CHINA_GRAPH_SCOPE,
"logs_endpoint": AZURE_CHINA_LOGS_ENDPOINT,
},
"AzureUSGovernment": {
"authority": AzureAuthorityHosts.AZURE_GOVERNMENT,
"base_url": AZURE_US_GOV_CLOUD,
"credential_scopes": [AZURE_US_GOV_CLOUD + "/.default"],
"graph_host": AZURE_US_GOV_GRAPH_HOST,
"graph_scope": AZURE_US_GOV_GRAPH_SCOPE,
"logs_endpoint": AZURE_US_GOV_LOGS_ENDPOINT,
},
}
return allowed_regions[region]
+30 -2
View File
@@ -1,5 +1,11 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider,
)
from msgraph.graph_request_adapter import GraphRequestAdapter
from msgraph_core import GraphClientFactory
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
@@ -47,10 +53,32 @@ class AzureService:
clients = {}
try:
if "GraphServiceClient" in str(service):
clients.update({identity.tenant_domain: service(credentials=session)})
# GraphServiceClient(credentials, scopes=...) only customises the
# OAuth scope; the underlying httpx client's base URL stays at
# graph.microsoft.com. For sovereign clouds we must also point
# the HTTP transport at the per-cloud host, which is done by
# building a custom GraphRequestAdapter with a NationalClouds
# base URL.
auth_provider = AzureIdentityAuthenticationProvider(
session, scopes=[region_config.graph_scope]
)
http_client = GraphClientFactory.create_with_default_middleware(
host=region_config.graph_host
)
request_adapter = GraphRequestAdapter(auth_provider, client=http_client)
clients.update(
{identity.tenant_domain: service(request_adapter=request_adapter)}
)
elif "LogsQueryClient" in str(service):
for subscription_id, display_name in identity.subscriptions.items():
clients.update({subscription_id: service(credential=session)})
clients.update(
{
subscription_id: service(
credential=session,
endpoint=region_config.logs_endpoint,
)
}
)
else:
for subscription_id, display_name in identity.subscriptions.items():
clients.update(
+3
View File
@@ -20,6 +20,9 @@ class AzureRegionConfig(BaseModel):
authority: Optional[str] = None
base_url: str = ""
credential_scopes: list = []
graph_host: str = "https://graph.microsoft.com"
graph_scope: str = "https://graph.microsoft.com/.default"
logs_endpoint: str = "https://api.loganalytics.io"
class AzureSubscription(BaseModel):
+21 -3
View File
@@ -402,6 +402,25 @@ class Provider(ABC):
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif arguments.provider == "stackit":
provider_class(
project_id=arguments.stackit_project_id,
service_account_key_path=getattr(
arguments, "stackit_service_account_key_path", None
),
service_account_key=getattr(
arguments, "stackit_service_account_key", None
),
regions=(
set(arguments.stackit_region)
if arguments.stackit_region
else None
),
scan_unused_services=arguments.scan_unused_services,
config_path=arguments.config_file,
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif arguments.provider == "github":
orgs = []
repos = []
@@ -549,8 +568,7 @@ class Provider(ABC):
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif "okta" in provider_class_name.lower():
elif arguments.provider == "okta":
provider_class(
okta_org_domain=getattr(arguments, "okta_org_domain", ""),
okta_client_id=getattr(arguments, "okta_client_id", ""),
@@ -563,7 +581,7 @@ class Provider(ABC):
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif "scaleway" in provider_class_name.lower():
elif arguments.provider == "scaleway":
# Credentials are read from the SCW_ACCESS_KEY /
# SCW_SECRET_KEY env vars by the provider itself; there
# are no credential CLI flags to avoid leaking secrets.
Whitespace-only changes.
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_admin_privilege_granted_alert_configured",
"CheckTitle": "User granted Admin privilege alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **User granted Admin privilege** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when a user is given elevated admin privileges.",
"Risk": "Without this alert enabled, administrators will not be notified when users receive **elevated admin privileges**. Unauthorized privilege escalation could indicate account compromise or insider threats and requires immediate verification.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **User granted Admin privilege** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **Medium**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **User granted Admin privilege** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_admin_privilege_granted_alert_configured"
}
},
"Categories": [
"logging",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "User granted Admin privilege"
class rules_admin_privilege_granted_alert_configured(Check):
"""Check that the User granted Admin privilege system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.googleworkspace.services.rules.rules_service import (
Rules,
)
rules_client = Rules(Provider.get_global_provider())
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_gmail_employee_spoofing_alert_configured",
"CheckTitle": "Gmail potential employee spoofing alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **Gmail potential employee spoofing** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when incoming messages have a sender name matching the directory but from an external domain.",
"Risk": "Without this alert enabled, administrators will not be notified of potential **employee spoofing via email**. Attackers may impersonate internal employees using external email addresses to conduct phishing attacks against the organization.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **Gmail potential employee spoofing** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **Medium**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **Gmail potential employee spoofing** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_gmail_employee_spoofing_alert_configured"
}
},
"Categories": [
"logging",
"email-security"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "Gmail potential employee spoofing"
class rules_gmail_employee_spoofing_alert_configured(Check):
"""Check that the Gmail potential employee spoofing system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_government_backed_attacks_alert_configured",
"CheckTitle": "Government-backed attacks alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **Government-backed attacks** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google believes users are being targeted by a government-backed attacker.",
"Risk": "Without this alert enabled, administrators will not be notified of potential **government-backed attacks** targeting their users. These attacks are sophisticated and require immediate response to protect affected accounts and investigate the threat.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **Government-backed attacks** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **High**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **Government-backed attacks** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_government_backed_attacks_alert_configured"
}
},
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "Government-backed attacks"
class rules_government_backed_attacks_alert_configured(Check):
"""Check that the Government-backed attacks system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_leaked_password_alert_configured",
"CheckTitle": "Leaked password alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **Leaked password** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google detects compromised credentials requiring a password reset.",
"Risk": "Without this alert enabled, administrators will not be notified when Google detects that a user's **credentials have been compromised** in a publicized breach. The user likely reused their password at another site that was breached, and their account requires an immediate password change.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **Leaked password** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **Medium**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **Leaked password** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_leaked_password_alert_configured"
}
},
"Categories": [
"logging",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "Leaked password"
class rules_leaked_password_alert_configured(Check):
"""Check that the Leaked password system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_password_changed_alert_configured",
"CheckTitle": "User's password changed alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **User's password changed** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are promptly notified when user passwords are changed.",
"Risk": "Without this alert enabled, administrators will not be notified when user passwords are changed. This could allow **credential compromise and account takeover** to go undetected, giving attackers time to establish persistence.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **User's password changed** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **Medium**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **User's password changed** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_password_changed_alert_configured"
}
},
"Categories": [
"logging",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "User's password changed"
class rules_password_changed_alert_configured(Check):
"""Check that the User's password changed system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,143 @@
from typing import Dict, List, Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService
SYSTEM_RULE_DEFAULTS: Dict[str, str] = {
"User's password changed": "INACTIVE",
"Government-backed attacks": "ACTIVE",
"User suspended due to suspicious activity": "ACTIVE",
"User granted Admin privilege": "INACTIVE",
"Suspicious programmatic login": "ACTIVE",
"Suspicious login": "ACTIVE",
"Leaked password": "ACTIVE",
"Gmail potential employee spoofing": "ACTIVE",
}
class Rules(GoogleWorkspaceService):
"""Google Workspace Rules service for auditing system-defined alert rules.
Uses the Cloud Identity Policy API v1 to read system-defined alert rule
configurations from the Admin Console "Rules" section.
"""
def __init__(self, provider):
super().__init__(provider)
self.system_defined_alerts: List[SystemDefinedAlert] = []
self.policies_fetched = False
self._fetch_system_defined_alerts()
def _fetch_system_defined_alerts(self):
"""Fetch system-defined alert rules from the Cloud Identity Policy API v1."""
logger.info("Rules - Fetching system-defined alert rules...")
try:
service = self._build_service("cloudidentity", "v1")
if not service:
logger.error("Failed to build Cloud Identity service")
return
request = service.policies().list(
pageSize=100,
filter='setting.type.matches("rule.system_defined_alerts")',
)
fetch_succeeded = True
found_rules: Dict[str, SystemDefinedAlert] = {}
while request is not None:
try:
response = request.execute()
for policy in response.get("policies", []):
if not self._is_customer_level_policy(policy):
continue
setting = policy.get("setting", {})
value = setting.get("value", {})
display_name = value.get("displayName", "")
if display_name not in SYSTEM_RULE_DEFAULTS:
continue
alert = self._parse_alert(value)
found_rules[display_name] = alert
logger.debug(
f"System-defined alert rule: {display_name} "
f"state={alert.state} "
f"has_recipients={alert.email_notifications_enabled}"
)
request = service.policies().list_next(request, response)
except Exception as error:
self._handle_api_error(
error,
"fetching system-defined alert rules",
self.provider.identity.customer_id,
)
fetch_succeeded = False
break
for rule_name, default_state in SYSTEM_RULE_DEFAULTS.items():
if rule_name not in found_rules:
is_active_default = default_state == "ACTIVE"
found_rules[rule_name] = SystemDefinedAlert(
display_name=rule_name,
state=default_state,
email_notifications_enabled=is_active_default,
all_super_admins=is_active_default,
)
logger.debug(
f"System-defined alert rule (default): {rule_name} "
f"state={default_state}"
)
self.system_defined_alerts = list(found_rules.values())
self.policies_fetched = fetch_succeeded
logger.info(
f"Rules policies fetched - "
f"{len(self.system_defined_alerts)} system-defined alert rules"
)
except Exception as error:
self._handle_api_error(
error,
"fetching system-defined alert rules",
self.provider.identity.customer_id,
)
self.policies_fetched = False
@staticmethod
def _parse_alert(value: dict) -> "SystemDefinedAlert":
"""Parse a single system-defined alert rule from the API response."""
display_name = value.get("displayName", "")
state = value.get("state", "INACTIVE")
alert_center_action = value.get("action", {}).get("alertCenterAction", {})
severity = alert_center_action.get("alertCenterConfig", {}).get("severity")
recipients = alert_center_action.get("recipients", [])
all_super_admins = any(r.get("allSuperAdmins") is True for r in recipients)
return SystemDefinedAlert(
display_name=display_name,
state=state,
severity=severity,
email_notifications_enabled=len(recipients) > 0,
all_super_admins=all_super_admins,
)
class SystemDefinedAlert(BaseModel):
"""Model for a system-defined alert rule."""
display_name: str
state: str = "INACTIVE"
severity: Optional[str] = None
email_notifications_enabled: bool = False
all_super_admins: bool = False
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_suspicious_activity_suspension_alert_configured",
"CheckTitle": "User suspended due to suspicious activity alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **User suspended due to suspicious activity** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google suspends an account due to a potential compromise.",
"Risk": "Without this alert enabled, administrators will not be promptly notified when Google **suspends a user account** due to detected compromise. The suspended user cannot work, and the underlying security incident requires immediate investigation.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **User suspended due to suspicious activity** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **High**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **User suspended due to suspicious activity** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_suspicious_activity_suspension_alert_configured"
}
},
"Categories": [
"logging",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "User suspended due to suspicious activity"
class rules_suspicious_activity_suspension_alert_configured(Check):
"""Check that the User suspended due to suspicious activity system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_suspicious_login_alert_configured",
"CheckTitle": "Suspicious login alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **Suspicious login** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google detects a sign-in attempt that does not match a user's normal behavior.",
"Risk": "Without this alert enabled, administrators will not be notified of **suspicious login attempts** such as sign-ins from unusual locations. This could indicate an active attack using previously obtained credentials.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **Suspicious login** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **Low**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **Suspicious login** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_suspicious_login_alert_configured"
}
},
"Categories": [
"logging",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "Suspicious login"
class rules_suspicious_login_alert_configured(Check):
"""Check that the Suspicious login system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "googleworkspace",
"CheckID": "rules_suspicious_programmatic_login_alert_configured",
"CheckTitle": "Suspicious programmatic login alert rule is configured",
"CheckType": [],
"ServiceName": "rules",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "monitoring",
"Description": "The **Suspicious programmatic login** system-defined alert rule should be enabled with alerts sent to the alert center, email notifications turned on, and recipients set to all super administrators. This ensures administrators are notified when Google detects suspicious login attempts from applications or programs.",
"Risk": "Without this alert enabled, administrators will not be notified of **suspicious programmatic login attempts**. This could indicate automated credential stuffing or unauthorized API access using compromised credentials.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/3230421",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Select **Rules**\n3. Under **Google protects you by default** select **View list**\n4. Scroll to **Suspicious programmatic login** and select it\n5. Within the Actions pane, click the edit pencil\n6. Select **Send to alert center** to set the alert to ON\n7. Set the alert severity to **Low**\n8. Select **Send email notifications**\n9. Ensure **All super administrators** is selected as recipients\n10. Click **Review** to confirm the values\n11. Click **Update Rule**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure the **Suspicious programmatic login** alert rule with alert center ON, email notifications ON, and recipients set to **all super administrators**.",
"Url": "https://hub.prowler.com/check/rules_suspicious_programmatic_login_alert_configured"
}
},
"Categories": [
"logging",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,61 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.rules.rules_client import (
rules_client,
)
RULE_NAME = "Suspicious programmatic login"
class rules_suspicious_programmatic_login_alert_configured(Check):
"""Check that the Suspicious programmatic login system-defined alert rule is fully configured."""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if rules_client.policies_fetched:
for alert in rules_client.system_defined_alerts:
if alert.display_name != RULE_NAME:
continue
domain = rules_client.provider.identity.domain
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=alert,
resource_id=f"systemDefinedAlert/{RULE_NAME}",
resource_name=RULE_NAME,
customer_id=rules_client.provider.identity.customer_id,
)
is_active = alert.state == "ACTIVE"
has_recipients = alert.email_notifications_enabled
all_super_admins = alert.all_super_admins
if is_active and has_recipients and all_super_admins:
report.status = "PASS"
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is properly "
f"configured in domain {domain}: alert is ON, email "
f"notifications are enabled, and recipients include "
f"all super administrators."
)
else:
report.status = "FAIL"
issues = []
if not is_active:
issues.append("alert is OFF")
if not has_recipients:
issues.append("email notifications are disabled")
elif not all_super_admins:
issues.append(
"email recipients do not include all super administrators"
)
report.status_extended = (
f"System-defined alert rule '{RULE_NAME}' is not properly "
f"configured in domain {domain}: {', '.join(issues)}."
)
findings.append(report)
return findings
Whitespace-only changes.
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "security_2sv_enforced",
"CheckTitle": "2-Step Verification is enforced for all users",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level policy **enforces 2-Step Verification (Multi-Factor Authentication)** for all users. 2-Step Verification requires users to present a second form of authentication beyond their password, significantly reducing the risk of account compromise.",
"Risk": "Without 2-Step Verification enforcement, users can access their accounts with **only a password**. If credentials are compromised through phishing, credential stuffing, or data breaches, attackers gain **immediate access** to the user's account and organizational data without any additional verification.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/security/protect-your-business-with-2-step-verification",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **2-Step Verification**\n3. Check **Allow users to turn on 2-Step Verification**\n4. Set **Enforcement** to **On**\n5. Set **New user enrollment period** to **2 weeks**\n6. Under **Frequency**, uncheck **Allow user to trust device**\n7. Under **Methods**, select **Any except verification codes via text, phone call**\n8. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enforce **2-Step Verification** for all users to require a second authentication factor beyond passwords, protecting accounts from credential-based attacks.",
"Url": "https://hub.prowler.com/check/security_2sv_enforced"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"security_2sv_hardware_keys_admins"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_2sv_enforced(Check):
"""Check that 2-Step Verification is enforced for all users.
This check verifies that the domain-level policy enforces 2-Step
Verification (Multi-Factor Authentication) for all users, reducing
the risk of account compromise through stolen credentials.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
enforced_from = security_client.policies.two_sv_enforced_from
# The API returns "1970-01-01T00:00:00Z" (protobuf zero-value
# Timestamp) when enforcement is OFF, not null or empty.
enforcement_off_epoch = "1970-01-01T00:00:00Z"
if enforced_from and enforced_from != enforcement_off_epoch:
report.status = "PASS"
report.status_extended = (
f"2-Step Verification enforcement is active "
f"(enforced from {enforced_from}) "
f"in domain {security_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if enforced_from is None:
report.status_extended = (
f"2-Step Verification enforcement is not configured "
f"in domain {security_client.provider.identity.domain}. "
f"The default is OFF. 2-Step Verification should be "
f"enforced for all users."
)
else:
report.status_extended = (
f"2-Step Verification enforcement is set to OFF "
f"in domain {security_client.provider.identity.domain}. "
f"2-Step Verification should be enforced for all users."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "security_2sv_hardware_keys_admins",
"CheckTitle": "Hardware security keys are required for 2-Step Verification",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level 2-Step Verification policy requires **hardware security keys only** as the allowed sign-in factor, providing the strongest phishing-resistant authentication. **Note**: the Policy API returns domain-wide policies only and cannot verify admin role-specific enforcement.",
"Risk": "When 2SV methods include **SMS, phone calls, or software-based authenticators**, users are vulnerable to **SIM swapping, SS7 attacks, and real-time phishing proxies** that can intercept one-time codes. Hardware security keys are resistant to all known remote phishing techniques.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/security/protect-your-business-with-2-step-verification",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Authentication** > **2-Step Verification**\n3. Select the appropriate group with **ALL ADMIN ROLES** (create this group if needed)\n4. Under **Methods**, select **Only security key**\n5. Under **2-Step Verification policy suspension grace period**, select **1 day**\n6. Under **Security codes**, select **Don't allow users to generate security codes**\n7. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Require **hardware security keys only** for 2-Step Verification to provide the strongest phishing-resistant authentication for all users, particularly those in administrative roles.",
"Url": "https://hub.prowler.com/check/security_2sv_hardware_keys_admins"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"security_2sv_enforced"
],
"Notes": "The Cloud Identity Policy API returns domain-wide policies only. It cannot verify that hardware keys are enforced specifically for admin roles versus all users. This check evaluates the customer-level enforcement factor, which applies to all users including administrators."
}
@@ -0,0 +1,64 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_2sv_hardware_keys_admins(Check):
"""Check that 2SV enforcement requires hardware security keys.
This check verifies that the domain-level 2-Step Verification enforcement
factor is set to security keys only, providing the strongest protection
against phishing attacks. Note: the Cloud Identity Policy API returns
domain-wide policies — it cannot verify enforcement for admin roles
specifically. This check evaluates the customer-level policy which
applies to all users including administrators.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
factor_set = security_client.policies.two_sv_allowed_factor_set
if factor_set == "PASSKEY_ONLY":
report.status = "PASS"
report.status_extended = (
f"2-Step Verification enforcement requires security keys only "
f"in domain {security_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if factor_set is None:
report.status_extended = (
f"2-Step Verification enforcement factor is not configured "
f"in domain {security_client.provider.identity.domain}. "
f"The default allows all methods including SMS and phone call. "
f"Security keys should be required for administrative accounts. "
f"Note: this check evaluates the domain-wide policy, the Policy "
f"API does not expose role-specific 2SV enforcement."
)
else:
report.status_extended = (
f"2-Step Verification enforcement factor is set to "
f"{factor_set} "
f"in domain {security_client.provider.identity.domain}. "
f"Only security keys (PASSKEY_ONLY) should be allowed for "
f"administrative accounts. "
f"Note: this check evaluates the domain-wide policy, the Policy "
f"API does not expose role-specific 2SV enforcement."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "security_advanced_protection_configured",
"CheckTitle": "Advanced Protection Program is configured",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level policy enables the **Advanced Protection Program** with user self-enrollment and **blocks the use of security codes**. The Advanced Protection Program is Google's strongest account security offering, requiring hardware security keys and applying a curated set of high-security policies.",
"Risk": "Without the Advanced Protection Program, user accounts rely on standard security controls that are vulnerable to **sophisticated phishing attacks, targeted credential theft, and third-party app data access**. Allowing security codes alongside Advanced Protection weakens its protections by providing an alternative authentication path that can be intercepted.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/security/protect-users-with-the-advanced-protection-program",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Advanced Protection Program**\n3. Under **Enrollment**, select **Enable user enrollment**\n4. Under **Security Codes**, select **Do not allow users to generate security codes**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the **Advanced Protection Program** with user self-enrollment and block **security codes** to enforce the strongest available account protection.",
"Url": "https://hub.prowler.com/check/security_advanced_protection_configured"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"security_2sv_enforced",
"security_2sv_hardware_keys_admins"
],
"Notes": ""
}
@@ -0,0 +1,66 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_advanced_protection_configured(Check):
"""Check that the Advanced Protection Program is configured.
This check verifies that the domain-level policy enables Advanced
Protection Program self-enrollment and blocks the use of security codes,
as recommended by CIS 4.1.3.1.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
enrollment = security_client.policies.advanced_protection_enrollment
code_option = (
security_client.policies.advanced_protection_security_code_option
)
domain = security_client.provider.identity.domain
enrollment_ok = enrollment is True
codes_ok = code_option == "CODES_NOT_ALLOWED"
if enrollment_ok and codes_ok:
report.status = "PASS"
report.status_extended = (
f"Advanced Protection Program is configured with enrollment "
f"enabled and security codes blocked in domain {domain}."
)
else:
report.status = "FAIL"
issues = []
if not enrollment_ok:
issues.append(
"enrollment is not configured"
if enrollment is None
else "enrollment is disabled"
)
if not codes_ok:
issues.append(
f"security codes are "
f"{code_option or 'using default (allowed without remote access)'} "
f"(should be CODES_NOT_ALLOWED)"
)
report.status_extended = (
f"Advanced Protection Program is not properly configured "
f"in domain {domain}: {'; '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "security_app_access_restricted",
"CheckTitle": "Application access to Google services is restricted",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level API controls configuration **restricts third-party app access** to at least one Google service. This check verifies that the administrator has configured API access controls rather than leaving all services at the unrestricted default. The CIS benchmark recommends restricting access to all applicable services, particularly high-risk scopes like Drive and Gmail.",
"Risk": "When application access to Google services is unrestricted, **any third-party app** that users consent to can access sensitive organizational data through Google APIs. This includes apps that may request **broad OAuth scopes** for Drive, Gmail, and other services, potentially leading to **data exfiltration** through unvetted applications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-data",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Access and Data Control** > **API Controls**\n3. Click **App access control** > **MANAGE GOOGLE SERVICES**\n4. Select **ALL applicable Google Services**\n5. Click **Change access**\n6. Select **Restricted: Only trusted apps can access a service**\n7. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **application access to Google services** to trusted apps only, particularly for high-risk scopes like **Drive and Gmail**, to prevent unvetted third-party apps from accessing sensitive organizational data.",
"Url": "https://hub.prowler.com/check/security_app_access_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"security_internal_apps_trusted"
],
"Notes": "This check verifies that at least one Google service has API access restricted, serving as a signal that the administrator has configured API access controls. The CIS benchmark recommends restricting access to all applicable services."
}
@@ -0,0 +1,62 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_app_access_restricted(Check):
"""Check that application access to Google services is restricted.
This check verifies that at least one Google service has API access
restricted for third-party apps, indicating that the administrator
has reviewed and configured API access controls. The CIS benchmark
recommends restricting access to all applicable services, particularly
high-risk scopes like Drive and Gmail. This check serves as a signal
that API access controls have been configured rather than left at the
unrestricted default.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
restricted = security_client.policies.google_services_restricted
domain = security_client.provider.identity.domain
if restricted is True:
report.status = "PASS"
report.status_extended = (
f"Application access to Google services is restricted "
f"in domain {domain}. At least one Google service has "
f"API access limited to trusted apps."
)
else:
report.status = "FAIL"
if restricted is None:
report.status_extended = (
f"Application access to Google services is not configured "
f"in domain {domain}. The default is unrestricted. "
f"API access should be restricted for all applicable "
f"Google services, particularly high-risk scopes."
)
else:
report.status_extended = (
f"Application access to Google services is unrestricted "
f"in domain {domain}. "
f"API access should be restricted for all applicable "
f"Google services, particularly high-risk scopes."
)
findings.append(report)
return findings
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.googleworkspace.services.security.security_service import (
Security,
)
security_client = Security(Provider.get_global_provider())
@@ -0,0 +1,37 @@
{
"Provider": "googleworkspace",
"CheckID": "security_dlp_drive_rules_configured",
"CheckTitle": "DLP policies for Google Drive are configured",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "At least one active **Data Loss Prevention (DLP) rule** targeting Google Drive file sharing is configured. DLP policies detect and prevent users from sharing sensitive information such as credit card numbers, identity numbers, and other regulated data through Drive.",
"Risk": "Without DLP policies, users can **freely share files containing sensitive information** through Google Drive without any detection or prevention controls. This increases the risk of **accidental data exposure, regulatory non-compliance**, and data breaches through oversharing.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/security/about-dlp",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Access and Data Control** > **Data protection**\n3. Click **Manage Rules**\n4. Click **ADD RULE** and select **New rule** or **New rule from template**\n5. Set the rule name and scope\n6. Set triggers by checking **File modified** under **Google Drive**\n7. Add conditions (Field, Comparison Operator, Content to match)\n8. Under **Actions**, select the desired action for each incident\n9. Under **Alerting**, set severity and select **Send to alert center**\n10. Click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure **DLP policies for Google Drive** to detect and prevent sharing of sensitive information such as credit card numbers, identity numbers, and other regulated data.",
"Url": "https://hub.prowler.com/check/security_dlp_drive_rules_configured"
}
},
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,50 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_dlp_drive_rules_configured(Check):
"""Check that DLP policies for Google Drive are configured.
This check verifies that at least one active Data Loss Prevention (DLP)
rule targeting Google Drive file sharing exists, helping to prevent
unintended exposure of sensitive information.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
dlp_exists = security_client.policies.dlp_drive_rules_exist
domain = security_client.provider.identity.domain
if dlp_exists is True:
report.status = "PASS"
report.status_extended = (
f"DLP policies for Google Drive are configured "
f"in domain {domain}. At least one active DLP rule "
f"targeting Drive file sharing exists."
)
else:
report.status = "FAIL"
report.status_extended = (
f"No active DLP policies for Google Drive are configured "
f"in domain {domain}. DLP rules should be configured "
f"to detect and prevent sharing of sensitive information "
f"through Drive."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "security_internal_apps_trusted",
"CheckTitle": "Internal apps can access Google Workspace APIs",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level API controls configuration **trusts internal domain-owned apps** to access restricted Google Workspace APIs. This avoids the need to individually trust each internal app.",
"Risk": "When internal apps are not trusted, legitimate **organization-built applications** cannot access restricted Google Workspace API scopes, potentially **breaking internal workflows** and forcing administrators to trust each app individually, which increases administrative overhead.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-data",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Access and Data Control** > **API Controls**\n3. Click **App access control** > **Settings**\n4. Check **Trust internal, domain-owned apps**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable **trust for internal domain-owned apps** so organization-built applications can access restricted Google Workspace APIs without individual trust configuration.",
"Url": "https://hub.prowler.com/check/security_internal_apps_trusted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"security_app_access_restricted"
],
"Notes": ""
}
@@ -0,0 +1,56 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_internal_apps_trusted(Check):
"""Check that internal apps can access Google Workspace APIs.
This check verifies that the domain-level policy trusts internal
domain-owned apps, allowing them to access restricted Google Workspace
APIs without requiring individual trust configuration.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
trust_internal = security_client.policies.trust_internal_apps
if trust_internal is True:
report.status = "PASS"
report.status_extended = (
f"Internal domain-owned apps are trusted to access "
f"Google Workspace APIs "
f"in domain {security_client.provider.identity.domain}."
)
elif trust_internal is None:
report.status = "PASS"
report.status_extended = (
f"Internal domain-owned apps use Google's secure default "
f"configuration (trusted) "
f"in domain {security_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Internal domain-owned apps are not trusted to access "
f"Google Workspace APIs "
f"in domain {security_client.provider.identity.domain}. "
f"Internal apps should be trusted to access restricted APIs."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "googleworkspace",
"CheckID": "security_less_secure_apps_disabled",
"CheckTitle": "Less secure app access is disabled",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level policy **disables access to less secure apps** that do not use modern security standards such as OAuth. Blocking these apps helps keep user accounts and organizational data safe.",
"Risk": "When less secure app access is enabled, users can allow apps that use **basic authentication** (username and password only) to access their Google account. These apps are more vulnerable to **credential theft** and do not support 2-Step Verification, increasing the risk of account compromise.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/apps/control-access-to-less-secure-apps",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Access and Data Control** > **Less secure apps**\n3. Select **Disable access to less secure apps (Recommended)**\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **less secure app access** to prevent users from allowing apps that do not use modern authentication standards to access their accounts.",
"Url": "https://hub.prowler.com/check/security_less_secure_apps_disabled"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,56 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_less_secure_apps_disabled(Check):
"""Check that less secure app access is disabled.
This check verifies that the domain-level policy prevents users from
allowing access to apps that use less secure sign-in technology,
reducing the risk of credential compromise.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
less_secure_allowed = security_client.policies.less_secure_apps_allowed
if less_secure_allowed is False:
report.status = "PASS"
report.status_extended = (
f"Less secure app access is disabled "
f"in domain {security_client.provider.identity.domain}."
)
elif less_secure_allowed is None:
report.status = "PASS"
report.status_extended = (
f"Less secure app access uses Google's secure default "
f"configuration (disabled) "
f"in domain {security_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Less secure app access is enabled "
f"in domain {security_client.provider.identity.domain}. "
f"Less secure app access should be disabled to prevent "
f"credential compromise through apps that do not use modern "
f"security standards."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "security_login_challenges_configured",
"CheckTitle": "Login challenges are configured correctly",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level login challenges configuration has the **employee ID challenge disabled**. CIS 4.1.4.1 also requires Post-SSO verification to be enabled, but that setting is **not exposed by the Cloud Identity Policy API**. This check only covers the employee ID challenge portion of the control.",
"Risk": "When the employee ID login challenge is enabled without proper configuration, it may create a **false sense of security** or interfere with the login flow. The employee ID challenge is a supplementary verification method that should only be used when specifically required by the organization.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/security/protect-google-workspace-accounts-with-security-challenges",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Login Challenges**\n3. Under **Login challenges**, uncheck **Use employee ID to keep my users more secure**\n4. Click **Save**\n5. Under **Post-SSO verification**, check **Logins using SSO are subject to additional verifications (if appropriate) and 2-Step Verification (if configured)** (this setting cannot be verified via the Policy API)\n6. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable the **employee ID login challenge** and manually verify that **Post-SSO verification** is enabled in the Admin Console, as the Post-SSO setting is not exposed by the Policy API.",
"Url": "https://hub.prowler.com/check/security_login_challenges_configured"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"security_2sv_enforced"
],
"Notes": "This check is partial — it only verifies the employee ID challenge setting. CIS 4.1.4.1 also requires Post-SSO verification to be enabled, which is not exposed by the Cloud Identity Policy API and must be verified manually."
}
@@ -0,0 +1,62 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_login_challenges_configured(Check):
"""Check that login challenges are configured correctly.
This check verifies that the employee ID login challenge is disabled,
as recommended by CIS. Note: CIS 4.1.4.1 also requires Post-SSO
verification to be enabled, but that setting is not exposed by the
Cloud Identity Policy API. This check only covers the employee ID
challenge portion of the control.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
employee_id_enabled = security_client.policies.login_challenge_employee_id
if employee_id_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Employee ID login challenge is disabled "
f"in domain {security_client.provider.identity.domain}. "
f"Note: Post-SSO verification status cannot be verified "
f"via the Policy API."
)
elif employee_id_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Employee ID login challenge uses Google's secure default "
f"configuration (disabled) "
f"in domain {security_client.provider.identity.domain}. "
f"Note: Post-SSO verification status cannot be verified "
f"via the Policy API."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Employee ID login challenge is enabled "
f"in domain {security_client.provider.identity.domain}. "
f"The employee ID challenge should be disabled per CIS "
f"recommendations. Note: Post-SSO verification status "
f"cannot be verified via the Policy API."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "googleworkspace",
"CheckID": "security_password_policy_strong",
"CheckTitle": "Password policy is configured for enhanced security",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level password policy is configured with **enhanced security settings**: minimum length of 14 characters, strong passwords enforced, password reuse disallowed, enforcement at next sign-in enabled, and password expiration set to 365 days.",
"Risk": "Weak password policies allow users to set **short, simple, or previously compromised passwords** that are vulnerable to brute-force attacks, credential stuffing, and password spraying. Without enforcement at sign-in, users may continue using weak passwords indefinitely.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/users/enforce-and-monitor-password-requirements-for-users",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Password management**\n3. Under **Strength**, check **Enforce strong passwords**\n4. Under **Length**, set **Minimum Length** to **14** or greater\n5. Under **Strength and Length enforcement**, check **Enforce password policy at next sign-in**\n6. Under **Reuse**, uncheck **Allow password reuse**\n7. Under **Expiration**, set **Password reset frequency** to **365 Days**\n8. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure a **strong password policy** with minimum 14-character length, strong password enforcement, reuse prevention, enforcement at next sign-in, and annual password expiration.",
"Url": "https://hub.prowler.com/check/security_password_policy_strong"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,76 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
class security_password_policy_strong(Check):
"""Check that password policy is configured for enhanced security.
This check verifies that the domain-level password policy meets CIS
requirements: minimum length of 14 characters, strong passwords enforced,
password reuse disallowed, enforcement at next sign-in, and password
expiration configured.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
policies = security_client.policies
domain = security_client.provider.identity.domain
issues = []
min_length = policies.password_minimum_length
if min_length is None or min_length < 14:
issues.append(
"minimum length is not configured (requires 14+)"
if min_length is None
else f"minimum length is {min_length} (requires 14+)"
)
if policies.password_allowed_strength != "STRONG":
issues.append(
"password strength is not configured (requires STRONG)"
if policies.password_allowed_strength is None
else f"password strength is {policies.password_allowed_strength} (requires STRONG)"
)
if policies.password_allow_reuse is True:
issues.append("password reuse is allowed")
if policies.password_enforce_at_login is not True:
issues.append("password policy is not enforced at next sign-in")
expiration = policies.password_expiration_duration
if expiration is None or expiration == "0s":
issues.append("password expiration is not configured")
if not issues:
report.status = "PASS"
report.status_extended = (
f"Password policy meets CIS requirements "
f"in domain {domain}: minimum length {min_length}, "
f"strong passwords enforced, reuse disallowed, "
f"enforced at next sign-in, expiration configured."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Password policy does not meet CIS requirements "
f"in domain {domain}: {'; '.join(issues)}."
)
findings.append(report)
return findings
@@ -0,0 +1,281 @@
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService
class Security(GoogleWorkspaceService):
"""Google Workspace Security service for auditing domain-level security policies.
Uses the Cloud Identity Policy API v1 to read authentication, password,
session, recovery, API control, and DLP settings configured in the
Admin Console.
"""
def __init__(self, provider):
super().__init__(provider)
self.policies = SecurityPolicies()
self.policies_fetched = False
self._fetch_security_policies()
def _fetch_security_policies(self):
"""Fetch security policies from the Cloud Identity Policy API v1."""
logger.info("Security - Fetching security policies...")
try:
service = self._build_service("cloudidentity", "v1")
if not service:
logger.error("Failed to build Cloud Identity service")
return
fetch_succeeded = True
# Fetch 1: security.* settings
fetch_succeeded = self._fetch_namespace(
service, 'setting.type.matches("security.*")', fetch_succeeded
)
# Fetch 2: api_controls.* settings
fetch_succeeded = self._fetch_namespace(
service, 'setting.type.matches("api_controls.*")', fetch_succeeded
)
# Fetch 3: rule.dlp for DLP existence check
fetch_succeeded = self._fetch_namespace(
service, 'setting.type.matches("rule.dlp")', fetch_succeeded
)
self.policies_fetched = fetch_succeeded
if fetch_succeeded:
logger.info("Security policies fetched successfully.")
else:
logger.warning(
"Security policies fetched with partial failures; "
"some checks may be skipped."
)
except Exception as error:
self._handle_api_error(
error,
"fetching security policies",
self.provider.identity.customer_id,
)
self.policies_fetched = False
def _fetch_namespace(self, service, filter_str: str, fetch_succeeded: bool) -> bool:
"""Fetch policies for a single namespace filter."""
try:
request = service.policies().list(
pageSize=100,
filter=filter_str,
)
while request is not None:
try:
response = request.execute()
for policy in response.get("policies", []):
if not self._is_customer_level_policy(policy):
continue
setting = policy.get("setting", {})
setting_type = setting.get("type", "").removeprefix("settings/")
value = setting.get("value", {})
self._process_setting(setting_type, value)
request = service.policies().list_next(request, response)
except Exception as error:
self._handle_api_error(
error,
f"fetching policies with filter {filter_str}",
self.provider.identity.customer_id,
)
return False
except Exception as error:
self._handle_api_error(
error,
f"listing policies with filter {filter_str}",
self.provider.identity.customer_id,
)
return False
return fetch_succeeded
def _process_setting(self, setting_type: str, value: dict):
"""Process a single policy setting and populate the model."""
# 2-Step Verification settings
if setting_type == "security.two_step_verification_enrollment":
self.policies.two_sv_allow_enrollment = value.get("allowEnrollment")
logger.debug(f"2SV enrollment: {self.policies.two_sv_allow_enrollment}")
elif setting_type == "security.two_step_verification_enforcement":
self.policies.two_sv_enforced_from = value.get("enforcedFrom")
logger.debug(f"2SV enforcement: {self.policies.two_sv_enforced_from}")
elif setting_type == "security.two_step_verification_enforcement_factor":
self.policies.two_sv_allowed_factor_set = value.get(
"allowedSignInFactorSet"
)
logger.debug(f"2SV factor set: {self.policies.two_sv_allowed_factor_set}")
elif setting_type == "security.two_step_verification_device_trust":
self.policies.two_sv_allow_trusting_device = value.get(
"allowTrustingDevice"
)
logger.debug(
f"2SV device trust: {self.policies.two_sv_allow_trusting_device}"
)
elif setting_type == "security.two_step_verification_grace_period":
self.policies.two_sv_enrollment_grace_period = value.get(
"enrollmentGracePeriod"
)
logger.debug(
f"2SV grace period: {self.policies.two_sv_enrollment_grace_period}"
)
elif setting_type == "security.two_step_verification_sign_in_code":
self.policies.two_sv_backup_code_exception_period = value.get(
"backupCodeExceptionPeriod"
)
logger.debug(
f"2SV backup code period: {self.policies.two_sv_backup_code_exception_period}"
)
# Account recovery
elif setting_type == "security.super_admin_account_recovery":
self.policies.super_admin_recovery_enabled = value.get(
"enableAccountRecovery"
)
logger.debug(
f"Super admin recovery: {self.policies.super_admin_recovery_enabled}"
)
elif setting_type == "security.user_account_recovery":
self.policies.user_recovery_enabled = value.get("enableAccountRecovery")
logger.debug(f"User recovery: {self.policies.user_recovery_enabled}")
# Advanced Protection Program
elif setting_type == "security.advanced_protection_program":
self.policies.advanced_protection_enrollment = value.get(
"enableAdvancedProtectionSelfEnrollment"
)
self.policies.advanced_protection_security_code_option = value.get(
"securityCodeOption"
)
logger.debug("Advanced Protection Program settings fetched.")
# Login challenges
elif setting_type == "security.login_challenges":
self.policies.login_challenge_employee_id = value.get(
"enableEmployeeIdChallenge"
)
logger.debug("Login challenges settings fetched.")
# Password policy
elif setting_type == "security.password":
self.policies.password_minimum_length = value.get("minimumLength")
self.policies.password_maximum_length = value.get("maximumLength")
self.policies.password_allowed_strength = value.get("allowedStrength")
self.policies.password_allow_reuse = value.get("allowReuse")
self.policies.password_enforce_at_login = value.get(
"enforceRequirementsAtLogin"
)
self.policies.password_expiration_duration = value.get("expirationDuration")
logger.debug("Password policy settings fetched.")
# Less secure apps
elif setting_type == "security.less_secure_apps":
self.policies.less_secure_apps_allowed = value.get("allowLessSecureApps")
logger.debug(f"Less secure apps: {self.policies.less_secure_apps_allowed}")
# Session controls
elif setting_type == "security.session_controls":
self.policies.web_session_duration = value.get("webSessionDuration")
logger.debug(f"Web session duration: {self.policies.web_session_duration}")
# Passkeys restriction
elif setting_type == "security.passkeys_restriction":
self.policies.passkeys_type = value.get("allowedPasskeysType")
logger.debug(f"Passkeys type: {self.policies.passkeys_type}")
# API controls - internal apps
elif setting_type == "api_controls.internal_apps":
self.policies.trust_internal_apps = value.get("trustInternalApps")
logger.debug(f"Trust internal apps: {self.policies.trust_internal_apps}")
# API controls - google services
elif setting_type == "api_controls.google_services":
services = value.get("services", [])
for svc in services:
if svc.get("isEnabled") is False:
self.policies.google_services_restricted = True
break
if self.policies.google_services_restricted is None:
self.policies.google_services_restricted = False
logger.debug(
f"Google services restricted: {self.policies.google_services_restricted}"
)
# DLP rules
elif setting_type == "rule.dlp":
state = value.get("state")
triggers = value.get("triggers", [])
if state == "ACTIVE" and any(
trigger.startswith("google.workspace.drive.") for trigger in triggers
):
self.policies.dlp_drive_rules_exist = True
logger.debug(f"DLP rule: state={state}, triggers={triggers}")
class SecurityPolicies(BaseModel):
"""Model for domain-level Security policy settings."""
# security.two_step_verification_enrollment
two_sv_allow_enrollment: Optional[bool] = None
# security.two_step_verification_enforcement
two_sv_enforced_from: Optional[str] = None
# security.two_step_verification_enforcement_factor
two_sv_allowed_factor_set: Optional[str] = None
# security.two_step_verification_device_trust
two_sv_allow_trusting_device: Optional[bool] = None
# security.two_step_verification_grace_period
two_sv_enrollment_grace_period: Optional[str] = None
# security.two_step_verification_sign_in_code
two_sv_backup_code_exception_period: Optional[str] = None
# security.super_admin_account_recovery
super_admin_recovery_enabled: Optional[bool] = None
# security.user_account_recovery
user_recovery_enabled: Optional[bool] = None
# security.advanced_protection_program
advanced_protection_enrollment: Optional[bool] = None
advanced_protection_security_code_option: Optional[str] = None
# security.login_challenges
login_challenge_employee_id: Optional[bool] = None
# security.password
password_minimum_length: Optional[int] = None
password_maximum_length: Optional[int] = None
password_allowed_strength: Optional[str] = None
password_allow_reuse: Optional[bool] = None
password_enforce_at_login: Optional[bool] = None
password_expiration_duration: Optional[str] = None
# security.less_secure_apps
less_secure_apps_allowed: Optional[bool] = None
# security.session_controls
web_session_duration: Optional[str] = None
# security.passkeys_restriction
passkeys_type: Optional[str] = None
# api_controls.internal_apps
trust_internal_apps: Optional[bool] = None
# api_controls.google_services
google_services_restricted: Optional[bool] = None
# rule.dlp
dlp_drive_rules_exist: Optional[bool] = None
@@ -0,0 +1,37 @@
{
"Provider": "googleworkspace",
"CheckID": "security_session_duration_limited",
"CheckTitle": "Google session control is configured to 12 hours or less",
"CheckType": [],
"ServiceName": "security",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "The domain-level Google session control limits web session duration to **12 hours or less**. When a session expires, users must re-authenticate, reducing the window of opportunity for session hijacking.",
"Risk": "The default 14-day session duration means that a compromised session token provides an attacker with **two weeks of uninterrupted access** without re-authentication. Shorter session durations limit the impact of **stolen cookies, session hijacking, and unauthorized access** from shared or untrusted devices.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/security/set-session-length-for-google-services",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Security** > **Access and Data Control** > **Google session control**\n3. Set **Web session duration** to **12 hours** or less\n4. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Set **Google session control** web session duration to **12 hours or less** to limit the window of access from compromised sessions.",
"Url": "https://hub.prowler.com/check/security_session_duration_limited"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,75 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.lib.logger import logger
from prowler.providers.googleworkspace.services.security.security_client import (
security_client,
)
MAX_SESSION_DURATION_SECONDS = 43200 # 12 hours
class security_session_duration_limited(Check):
"""Check that Google session control is configured to 12 hours or less.
This check verifies that the domain-level web session duration is set
to 12 hours or less, requiring users to re-authenticate more frequently
than the default 14-day session length.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if security_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=security_client.policies,
resource_id="securityPolicies",
resource_name="Security Policies",
customer_id=security_client.provider.identity.customer_id,
)
duration_str = security_client.policies.web_session_duration
domain = security_client.provider.identity.domain
if duration_str is None:
report.status = "FAIL"
report.status_extended = (
f"Google session control is not explicitly configured "
f"in domain {domain}. The default is 14 days. "
f"Web session duration should be 12 hours or less."
)
findings.append(report)
return findings
try:
duration_seconds = int(duration_str.removesuffix("s"))
except ValueError:
logger.error(f"Unparseable web session duration: {duration_str!r}")
report.status = "FAIL"
report.status_extended = (
f"Web session duration value {duration_str!r} is not parseable "
f"in domain {domain}."
)
findings.append(report)
return findings
duration_hours = duration_seconds / 3600
if duration_seconds <= MAX_SESSION_DURATION_SECONDS:
report.status = "PASS"
report.status_extended = (
f"Google session control is set to {duration_hours:.0f} hours "
f"in domain {domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Google session control is set to {duration_hours:.0f} hours "
f"in domain {domain}. "
f"Web session duration should be 12 hours or less."
)
findings.append(report)
return findings
Loaded 100 of 306 files, more files were not shown because too many files have changed in this diff. Show more