Compare commits

..

6 Commits

Author SHA1 Message Date
Prowler Bot a7f4f44e7b fix(docker): chown copied files to prowler pin uv sync --locked (#11242)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-05-19 18:13:19 +02:00
Prowler Bot 2a31bfc3e6 chore(stepsecurity): add missing endpoints (#11241)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-05-19 18:11:52 +02:00
Prowler Bot 1a4cfd81c5 fix(azure): skip system 'master' DB in sqlserver_tde_encrypted_with_cmk (#11235)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-05-19 17:05:35 +02:00
Prowler Bot c0559e7f10 fix(s3): only emit shadow-resource finding when bucket name matches a predictable pattern (#11237)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-05-19 15:53:59 +01:00
Prowler Bot 706742e6dc chore(release): Bump versions to v5.27.1 (#11226)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-05-19 15:11:49 +02:00
Prowler Bot baaf56ea5e chore(api): Update prowler dependency to v5.27 for release 5.27.0 (#11219)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-05-19 12:17:44 +02:00
26 changed files with 98 additions and 1022 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.28.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.27.1
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
@@ -133,17 +133,11 @@ jobs:
with:
persist-credentials: false
- name: Pin prowler SDK to latest master commit and refresh lockfile
- name: Pin prowler SDK to latest master commit
if: github.event_name == 'push'
run: |
set -e
LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1)
sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml
# Refresh api/uv.lock so it matches the pinned SHA above; the API
# Dockerfile runs `uv sync --locked`, which aborts on any drift
# between pyproject.toml and uv.lock.
pip install --no-cache-dir "uv==0.11.14"
(cd api && uv lock)
- name: Login to DockerHub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
+6 -6
View File
@@ -66,7 +66,7 @@ jobs:
title: ${{ steps.compute-text.outputs.title }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: audit
@@ -135,7 +135,7 @@ jobs:
secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: audit
@@ -870,7 +870,7 @@ jobs:
total_count: ${{ steps.missing_tool.outputs.total_count }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: audit
@@ -982,7 +982,7 @@ jobs:
success: ${{ steps.parse_results.outputs.success }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: audit
@@ -1091,7 +1091,7 @@ jobs:
activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: audit
@@ -1164,7 +1164,7 @@ jobs:
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: audit
+2 -2
View File
@@ -89,7 +89,7 @@ WORKDIR /home/prowler
# Ensure output directory exists
RUN mkdir -p /tmp/prowler_api_output
COPY --chown=prowler:prowler pyproject.toml uv.lock ./
COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir uv==0.11.14
@@ -97,7 +97,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
ENV PATH="/home/prowler/.local/bin:$PATH"
# Add `--no-install-project` to avoid installing the current project as a package
RUN uv sync --locked --no-install-project && \
RUN uv sync --no-install-project && \
rm -rf ~/.cache/uv
RUN .venv/bin/python .venv/lib/python3.12/site-packages/prowler/providers/m365/lib/powershell/m365_powershell.py
+2 -2
View File
@@ -43,7 +43,7 @@ dependencies = [
"defusedxml==0.7.1",
"gunicorn==23.0.0",
"lxml==6.1.0",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.27",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
@@ -68,7 +68,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.29.0"
version = "1.28.1"
[tool.uv]
# Transitive pins matching master to avoid silent drift; bump deliberately.
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.29.0
version: 1.28.1
description: |-
Prowler API specification.
Generated
+3 -2
View File
@@ -4411,7 +4411,7 @@ wheels = [
[[package]]
name = "prowler"
version = "5.27.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#0abbb7fc590eaf7de6ed354dd5a217bca261d2b0" }
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.27#cb01769237cb99a21f2f12cce470e239573e1a01" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4484,6 +4484,7 @@ dependencies = [
{ name = "pygithub" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "scaleway" },
{ name = "schema" },
{ name = "shodan" },
{ name = "slack-sdk" },
@@ -4590,7 +4591,7 @@ requires-dist = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.27" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
@@ -118,8 +118,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.27.0"
PROWLER_API_VERSION="5.27.0"
PROWLER_UI_VERSION="5.26.1"
PROWLER_API_VERSION="5.26.1"
```
<Note>
-2
View File
@@ -9,8 +9,6 @@ Prowler Cloud runs on AWS with high availability built in.
| Region | URL | Location |
|--------|-----|----------|
| **EU** | [cloud.prowler.com](https://cloud.prowler.com) | Ireland (`eu-west-1`) |
| **US** | On-Demand | On-Demand |
## Business Continuity
+1 -1
View File
@@ -14,7 +14,7 @@ All Prowler code goes through the same security pipeline, whether running on Pro
Security tools and practices applied to all Prowler code.
</Card>
## Prowler Cloud vs Prowler OSS (Self-Managed)
## Prowler Cloud vs Self-Managed
| | Prowler Cloud | Self-Managed |
|--|---------------|--------------|
+60 -146
View File
@@ -2,183 +2,97 @@
title: 'Software Security'
---
Prowler applies security-by-design across the development lifecycle. Every change passes automated checks at each stage: pre-commit hooks locally, multiple CI gates on pull requests, branch protection before merge, container scanning before publish, and registry monitoring after release.
Prowler follows a **security-by-design approach** throughout the software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment.
All security tooling and configuration lives in the [Prowler GitHub repository](https://github.com/prowler-cloud/prowler): [pre-commit hooks](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml), [CI/CD workflows](https://github.com/prowler-cloud/prowler/tree/master/.github/workflows), and [Dependabot configuration](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml).
[Pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations catch issues early, and [CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates ensuring code quality, secure configurations, and compliance with internal standards.
## Coverage
Security controls cover six domains, each detailed below:
| Domain | What It Protects |
|--------|------------------|
| [**CI/CD**](#cicd-security) | GitHub Actions workflows, runners, third-party actions |
| [**SAST**](#static-application-security-testing-sast) | Application source code |
| [**SCA**](#software-composition-analysis-sca) | Third-party dependencies and their known vulnerabilities |
| [**Supply-Chain Pinning**](#supply-chain-pinning) | Reproducible installs across Python, npm, GitHub Actions, container base images |
| [**Containers**](#container-security) | Runtime images for UI, API, SDK, Model Context Protocol (MCP) Server |
| [**Secrets**](#secrets-detection) | Credentials, tokens, API keys in code and git history |
## CI/CD Security
Every GitHub Actions workflow uses runner hardening, pinned action versions, and audited permissions.
### Runner Hardening With StepSecurity
- [**`step-security/harden-runner`**](https://github.com/step-security/harden-runner) runs as the first step in every workflow, pinned by commit SHA.
- Workflows are being migrated to explicit egress controls: some already declare an egress allow-list with `egress-policy: block`, while others still run in `egress-policy: audit` until their allowed endpoints are fully defined.
- **Global Block Policy** (StepSecurity) blocks known-malicious domains and IP addresses across every workflow run. This protection applies even in audit mode, so workflows that have not yet moved to `block` still resist known-bad egress.
### Third-Party Action Pinning
- Every third-party action reference uses a commit SHA with the version as a comment: `uses: org/action@<sha> # v1.2.3`.
- Dependabot tracks the comment and proposes SHA-pinned upgrades on a monthly cadence.
### Workflow Permissions
- Workflows declare `permissions: {}` at the top level and grant the minimum required scopes per job.
- Code review covers permission changes; zizmor enforces the rules (see below).
### Workflow Security Audit With Zizmor
- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs via [`ci-zizmor.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ci-zizmor.yml).
- Triggers on every push, every pull request that touches `.github/`, and on a daily schedule.
- Results upload to the GitHub Security tab via Static Analysis Results Interchange Format (SARIF).
- Key [audit rules](https://docs.zizmor.sh/audits/) the build gates on:
- **[PWN Request](https://docs.zizmor.sh/audits/#dangerous-triggers)** (`dangerous-triggers`): unsafe use of `pull_request_target` with checked-out PR code.
- **[Script Injection](https://docs.zizmor.sh/audits/#template-injection)** (`template-injection`): unsanitized `${{ github.event.* }}` expressions in `run:` blocks.
- **[artipacked](https://docs.zizmor.sh/audits/#artipacked)**: credential leakage through artifacts.
- **[Excessive permissions](https://docs.zizmor.sh/audits/#excessive-permissions)** (`excessive-permissions`): workflows with unneeded `write` scopes.
### Branch Protection
Pull requests to `master` and the active `v5.*` release branches must pass several required workflows before merge. These gates prevent specific classes of supply-chain and pipeline attacks from reaching the main branch:
- **Compromised packages:** the **npm Package Compromised Updates** and **PyPI Package Compromised Updates** checks (StepSecurity) fail any PR that introduces a package version present in the compromised-package feed. Layered on top of osv-scanner.
- **Premature releases:** the **npm Package Cooldown** and **PyPI Package Cooldown** checks (StepSecurity) fail any PR that introduces a package version published within the cooldown window. Layered on top of pnpm's `minimumReleaseAge`.
- **Workflow exploitation:** the **PWN Request** and **Script Injection** checks (StepSecurity) reject the corresponding zizmor-detected anti-patterns at PR time. Layered on top of zizmor's audit.
- **Vulnerable code or dependencies:** CodeQL (UI, API, SDK), osv-scanner (SDK, API, UI), Bandit (SDK, API), and Trivy (container images) must all pass.
Container registries are continuously scanned for vulnerabilities, with findings automatically reported to the security team for assessment and remediation. This process evolves alongside the stack as new languages, frameworks, and technologies are adopted, ensuring security practices remain comprehensive, proactive, and adaptable.
## Static Application Security Testing (SAST)
Multiple SAST tools run on every push and pull request to catch vulnerabilities and code-quality issues before merge.
Multiple SAST tools are employed across the codebase to identify security vulnerabilities, code quality issues, and potential bugs during development.
### Cross-Language
### CodeQL Analysis
- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan, via [`sdk-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-codeql.yml), [`api-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-codeql.yml), and [`ui-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-codeql.yml). Results upload to the GitHub Security tab via SARIF.
- **Scope:** UI (JavaScript/TypeScript), API (Python), and SDK (Python)
- **Frequency:** On every push and pull request, plus daily scheduled scans
- **Integration:** Results uploaded to GitHub Security tab via SARIF format
- **Purpose:** Identifies security vulnerabilities, coding errors, and potential exploits in source code
### Python (SDK + API)
### Python Security Scanners
- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every PR/push in [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml) and [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml).
- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml) and [`api-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-code-quality.yml).
- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every PR/push in `sdk-security.yml` and `api-security.yml`.
- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml).
- **Bandit:** Detects common security issues in Python code (SQL injection, hardcoded passwords, etc.)
- Configured to ignore test files and report only high-severity issues
- Runs on both SDK and API codebases
- **Pylint:** Static code analysis with security-focused checks
- Integrated into pre-commit hooks and CI/CD pipelines
### JavaScript/TypeScript (UI)
### Code Quality & Dead Code Detection
- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every PR/push via `pnpm run healthcheck` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml).
- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every PR/push via `pnpm run healthcheck` in `ui-tests.yml`.
- **Knip:** dead-code and unused-export detection for the UI. The UI analogue to Vulture.
<Note>
Knip runs locally on demand via `pnpm run lint:knip` and is not yet wired into CI.
</Note>
### Shell
- **Shellcheck:** correctness and security checks for shell scripts in `.github/scripts/` and `scripts/`. Runs in pre-commit on staged files.
- **Vulture:** Identifies unused code that could indicate incomplete implementations or security gaps
- **Flake8:** Style guide enforcement with security-relevant checks
- **Shellcheck:** Security and correctness checks for shell scripts
## Software Composition Analysis (SCA)
Dependencies are scanned against public vulnerability databases on every pull request and push, with results posted directly on the PR.
Dependencies are continuously monitored for known vulnerabilities with timely updates ensured.
### Cross-Language
### Dependency Vulnerability Scanning
- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs via [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml), [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml), and [`ui-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-security.yml).
- The action installs the `osv-scanner` binary and verifies its SHA-256 checksum against the upstream-signed `SHA256SUMS` manifest before running. Any mismatch aborts the scan.
- Gates the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings.
- Posts and updates a per-lockfile report as a pull request comment.
- Per-vulnerability ignores live in [`osv-scanner.toml`](https://github.com/prowler-cloud/prowler/blob/master/osv-scanner.toml) at the repo root, each with a reason and an expiry date.
- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR.
- **Dependabot:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml) for monthly updates of the SDK Python dependencies, GitHub Actions, Docker base images, and pre-commit hooks. Dependabot opens pull requests for known security advisories, so critical patches reach the team without delay. A 7-day default cooldown reduces exposure to compromised package releases.
- **osv-scanner:** Scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database
- Runs in CI on every pull request and push for SDK, API, and UI
- Fails the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings
- Posts a per-lockfile report as a PR comment
- Per-vulnerability ignores (with reason and expiry) live in `osv-scanner.toml` at the repo root
- **Trivy:** Multi-purpose scanner for containers and dependencies
- Scans all container images (UI, API, SDK, MCP Server)
- Checks for vulnerabilities in OS packages and application dependencies
- Reports findings to GitHub Security tab
### JavaScript/TypeScript (UI)
### Automated Dependency Updates
- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml). Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier.
## Supply-Chain Pinning
Pinning runs across Python, npm, GitHub Actions, and container base images. Every install resolves to the exact set of versions already vetted in CI, and any drift fails loudly instead of slipping in silently.
### Python (uv)
The SDK, API, and MCP Server all use [uv](https://docs.astral.sh/uv/) for dependency management. Each component has its own project manifest and lock file:
| Component | Manifest | Lock File |
|-----------|----------|-----------|
| SDK | `pyproject.toml` | `uv.lock` |
| API | `api/pyproject.toml` | `api/uv.lock` |
| MCP Server | `mcp_server/pyproject.toml` | `mcp_server/uv.lock` |
The controls applied across all three:
- **Direct dependencies pinned to exact versions** (`==`). No version ranges in dependency lists.
- **Transitive dependencies pinned** via `[tool.uv].constraint-dependencies` in the SDK and API manifests. The constraint set mirrors the versions locked in the corresponding `uv.lock`. A future `uv lock` preserves these versions instead of silently picking up newer releases, and the resolver fails when a constraint becomes infeasible, signaling that a deliberate bump is needed.
- **Lock files committed.** CI installs strictly from the lock.
- **uv itself pinned** in the [`setup-python-uv`](https://github.com/prowler-cloud/prowler/tree/master/.github/actions/setup-python-uv) composite action.
<Note>
The MCP Server has a small direct-dependency surface and does not yet declare a separate constraint set. Its lock file is the source of truth.
</Note>
### JavaScript/TypeScript (pnpm)
The UI uses [pnpm](https://pnpm.io) with supply-chain controls configured in [`ui/pnpm-workspace.yaml`](https://github.com/prowler-cloud/prowler/blob/master/ui/pnpm-workspace.yaml).
- **Minimum release age** (`minimumReleaseAge: 1440`): packages must publish at least 24 hours before install. This reduces exposure during the window when a compromised release has not yet been detected and yanked.
- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `@heroui/shared-utils`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install.
- **Trust policy** (`trustPolicy: no-downgrade`): the install fails when a package's trust evidence drops, for example after a new publisher takes over.
- **Block exotic subdeps** (`blockExoticSubdeps: true`): transitive dependencies cannot ship as git URLs or tarballs. Every package in the tree resolves from the configured registry.
- **Transitive overrides** in [`ui/package.json`](https://github.com/prowler-cloud/prowler/blob/master/ui/package.json) force specific versions for transitive packages (`lodash`, `serialize-javascript`, `qs`, `rollup`, `minimatch`, `ajv`, and others).
- **`pnpm-lock.yaml` committed** and CI installs strictly from the lock.
- **pnpm itself pinned** via the `packageManager` field in `package.json` with an integrity hash.
### GitHub Actions
- Every third-party action reference uses a commit SHA, with the version in a trailing comment.
- Dependabot opens monthly PRs to bump pinned SHAs.
### Container Base Images
- Every Dockerfile references base images by digest (`image@sha256:...`).
- Dependabot opens monthly PRs to bump digests.
- **Dependabot:** Automated pull requests for dependency updates
- **Python (pip):** Monthly updates for SDK
- **GitHub Actions:** Monthly updates for workflow dependencies
- **Docker:** Monthly updates for base images
- Temporarily paused for API and UI to maintain stability during active development
- **Security-first approach:** Even when paused, Dependabot automatically creates pull requests for security vulnerabilities, ensuring critical security patches are never delayed
## Container Security
Container images get scanned twice: once in CI before they push to a registry, and continuously after publish by the registries themselves.
All container images are scanned before deployment.
### Pre-Publish (CI)
### Trivy Vulnerability Scanning
- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds can fail on critical findings when configured to.
- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and in the same `*-container-checks.yml` workflows linked above.
- Scans images for vulnerabilities and misconfigurations
- Generates SARIF reports uploaded to GitHub Security tab
- Creates PR comments with scan summaries
- Configurable to fail builds on critical findings
- Reports include CVE counts and remediation guidance
### Post-Publish (Registries)
### Hadolint
- **Amazon ECR:** ECR continuously scans published images for vulnerabilities. New advisories disclosed after publish surface on the image without requiring a rebuild.
- **Docker Hub:** Docker Hub continuously scans the same images mirrored from ECR.
- The security team reviews findings from both registries for triage and remediation.
- Validates Dockerfile syntax and structure
- Ensures secure image building practices
## Secrets Detection
- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request via [`find-secrets.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/find-secrets.yml). Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings.
- A pre-commit hook runs the same check locally and blocks secrets before they leave the developer machine.
Prowler protects against accidental exposure of sensitive credentials.
### TruffleHog
- Scans entire codebase and Git history for secrets
- Runs on every push and pull request
- Pre-commit hook prevents committing secrets
- Detects high-entropy strings, API keys, tokens, and credentials
- Configured to report verified and unknown findings
## Security Monitoring
- **GitHub Security tab:** centralized view of findings from CodeQL, Trivy, zizmor, and any other SARIF-compatible tool.
- **PR comments:** osv-scanner and Trivy post per-PR summaries so issues surface during review, not after merge.
- **Artifact retention:** the build retains security scan reports for post-deployment analysis.
- **GitHub Security Tab:** Centralized view of all security findings from CodeQL, Trivy, and other SARIF-compatible tools
- **Artifact Retention:** Security scan reports retained for post-deployment analysis
- **PR Comments:** Automated security feedback on pull requests for rapid remediation
## Contact
For questions about software security, see the [Support page](/support). To report a vulnerability, follow the [responsible disclosure process](https://prowler.com/.well-known/security.txt).
For questions regarding software security, visit the [Support page](/support).
-9
View File
@@ -2,15 +2,6 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.28.0] (Prowler UNRELEASED)
### 🚀 Added
- `entra_app_registration_client_secret_unused` check for M365 provider [(#11232)](https://github.com/prowler-cloud/prowler/pull/11232)
- `cloudsql_instance_cmek_encryption_enabled` check for GCP provider [(#11023)](https://github.com/prowler-cloud/prowler/pull/11023)
---
## [5.27.1] (Prowler UNRELEASED)
### 🐞 Fixed
+1 -1
View File
@@ -48,7 +48,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.28.0"
prowler_version = "5.27.1"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
@@ -1,42 +0,0 @@
{
"Provider": "gcp",
"CheckID": "cloudsql_instance_cmek_encryption_enabled",
"CheckTitle": "Cloud SQL instance is encrypted with a customer-managed key (CMEK)",
"CheckType": [],
"ServiceName": "cloudsql",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "sqladmin.googleapis.com/Instance",
"Description": "**Cloud SQL instances** use **customer-managed encryption keys** (`CMEK`) via Cloud KMS for at-rest encryption. The evaluation identifies instances lacking a configured **Cloud KMS key**, indicating use of default Google-managed encryption instead.",
"Risk": "Without CMEK, Google holds sole control of the encryption keys. If the organization must demonstrate key custody, meet data residency requirements, or immediately revoke access to data (e.g., upon contract termination), Google-managed keys are insufficient. This may violate ISMS-P 2.7.1 and regulatory requirements for sensitive or personal data.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://cloud.google.com/sql/docs/mysql/cmek",
"https://cloud.google.com/sql/docs/postgres/cmek",
"https://cloud.google.com/sql/docs/sqlserver/cmek",
"https://cloud.google.com/kms/docs/resource-hierarchy"
],
"Remediation": {
"Code": {
"CLI": "gcloud sql instances create <INSTANCE_NAME> \\\n --database-version=<DATABASE_VERSION> \\\n --region=<REGION> \\\n --disk-encryption-key=projects/<PROJECT>/locations/<REGION>/keyRings/<RING>/cryptoKeys/<KEY>",
"NativeIaC": "",
"Other": "CMEK must be configured at instance creation time. To migrate an existing instance:\n1. Create a new Cloud SQL instance with CMEK enabled.\n2. Export data from the existing instance.\n3. Import data into the new CMEK-enabled instance.\n4. Update application connection strings.",
"Terraform": "```hcl\nresource \"google_sql_database_instance\" \"example\" {\n name = \"<instance_name>\"\n database_version = \"<database_version>\"\n region = \"<region>\"\n\n encryption_key_name = \"projects/<project>/locations/<region>/keyRings/<ring>/cryptoKeys/<key>\"\n\n settings {\n tier = \"db-custom-2-7680\"\n }\n}\n```"
},
"Recommendation": {
"Text": "For instances storing personal or sensitive data, create new Cloud SQL instances with CMEK using a Cloud KMS key in the same region. Ensure the Cloud SQL service account has the roles/cloudkms.cryptoKeyEncrypterDecrypter role on the key, and enable key rotation per your policy.",
"Url": "https://hub.prowler.com/check/cloudsql_instance_cmek_encryption_enabled"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [
"kms_key_rotation_enabled",
"kms_key_not_publicly_accessible",
"bigquery_dataset_cmk_encryption"
],
"Notes": "CMEK cannot be enabled on an existing Cloud SQL instance; it must be set at creation time. Existing instances require data migration to a new CMEK-enabled instance."
}
@@ -1,25 +0,0 @@
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.cloudsql.cloudsql_client import cloudsql_client
class cloudsql_instance_cmek_encryption_enabled(Check):
def execute(self) -> Check_Report_GCP:
findings = []
for instance in cloudsql_client.instances:
if instance.instance_type != "CLOUD_SQL_INSTANCE":
continue
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
if instance.cmek_key_name:
report.status = "PASS"
report.status_extended = (
f"Database instance {instance.name} is encrypted with "
f"customer-managed key: {instance.cmek_key_name}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Database instance {instance.name} is not encrypted with a "
f"customer-managed key (CMEK); Google-managed key is in use."
)
findings.append(report)
return findings
@@ -1,5 +1,3 @@
from typing import Optional
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
@@ -26,8 +24,6 @@ class CloudSQL(GCPService):
for address in instance.get("ipAddresses", []):
if address["type"] == "PRIMARY":
public_ip = True
settings = instance.get("settings", {})
ip_config = settings.get("ipConfiguration", {})
self.instances.append(
Instance(
name=instance["name"],
@@ -35,23 +31,19 @@ class CloudSQL(GCPService):
region=instance["region"],
ip_addresses=instance.get("ipAddresses", []),
public_ip=public_ip,
require_ssl=ip_config.get("requireSsl", False),
ssl_mode=ip_config.get(
"sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED"
),
automated_backups=settings.get(
"backupConfiguration", {}
).get("enabled", False),
authorized_networks=ip_config.get(
"authorizedNetworks", []
),
flags=settings.get("databaseFlags", []),
instance_type=instance.get(
"instanceType", "CLOUD_SQL_INSTANCE"
),
cmek_key_name=instance.get(
"diskEncryptionConfiguration", {}
).get("kmsKeyName"),
require_ssl=instance["settings"]
.get("ipConfiguration", {})
.get("requireSsl", False),
ssl_mode=instance["settings"]
.get("ipConfiguration", {})
.get("sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED"),
automated_backups=instance["settings"]
.get("backupConfiguration", {})
.get("enabled", False),
authorized_networks=instance["settings"]
.get("ipConfiguration", {})
.get("authorizedNetworks", []),
flags=instance["settings"].get("databaseFlags", []),
project_id=project_id,
)
)
@@ -76,6 +68,4 @@ class Instance(BaseModel):
ssl_mode: str
automated_backups: bool
flags: list
instance_type: str = "CLOUD_SQL_INSTANCE"
cmek_key_name: Optional[str] = None
project_id: str
@@ -1,42 +0,0 @@
{
"Provider": "m365",
"CheckID": "entra_app_registration_client_secret_unused",
"CheckTitle": "Application registrations should not use client secrets",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra **application registrations** should not have any **secret credentials** configured. This check audits the current state of every application registration and reports those that hold one or more **secrets**. Both **expired** and **active** secrets are reported, since expired entries are credential clutter that should be cleaned up.",
"Risk": "**Secrets** are bearer credentials that are easy to leak (committed to repositories, copied into CI variables, shared via chat). Once leaked, a **secret** can be used from anywhere on the internet until it is rotated. Both **expired** and **active** secrets increase the attack surface and represent credential clutter.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials",
"https://learn.microsoft.com/en-us/graph/api/resources/passwordcredential?view=graph-rest-1.0",
"https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation",
"https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Applications > App registrations\n3. Select the flagged application\n4. Go to Certificates & secrets\n5. Delete all client secrets under the 'Client secrets' tab\n6. Add a certificate or configure federated identity credentials instead",
"Terraform": ""
},
"Recommendation": {
"Text": "Remove every **secret** from the affected **application registrations** so they no longer rely on long-lived shared secrets. Enable the **default app management policy** to block new secrets from being added.",
"Url": "https://hub.prowler.com/check/entra_app_registration_client_secret_unused"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [
"entra_default_app_management_policy_enabled"
],
"Notes": "This check audits the current state of password credentials on app registrations. The related check entra_default_app_management_policy_enabled audits the tenant-wide policy that prevents new secrets from being added. Both expired and active password credentials trigger a FAIL."
}
@@ -1,58 +0,0 @@
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_app_registration_client_secret_unused(Check):
"""
Ensure that application registrations do not use password credentials (client secrets).
This check evaluates application registrations in Microsoft Entra ID to identify
those with password credentials (client secrets). Applications should authenticate
using certificates, federated identity credentials, or managed identities instead.
Both expired and active password credentials are flagged, since expired entries are
credential clutter that should be cleaned up.
- PASS: The application has no password credentials.
- FAIL: The application has one or more password credentials that should be removed.
"""
def execute(self) -> list[CheckReportM365]:
findings = []
for app_id, app in entra_client.app_registrations.items():
report = CheckReportM365(
metadata=self.metadata(),
resource=app,
resource_name=app.name or app.app_id,
resource_id=app_id,
)
num_secrets = len(app.password_credentials)
if num_secrets > 0:
report.status = "FAIL"
secret_details = []
for cred in app.password_credentials:
detail = cred.display_name or cred.key_id
if cred.end_date_time:
detail += f" (expires: {cred.end_date_time})"
secret_details.append(detail)
if num_secrets > 5:
displayed = ", ".join(secret_details[:5])
displayed += f" (and {num_secrets - 5} more)"
else:
displayed = ", ".join(secret_details)
report.status_extended = (
f"App registration {app.name} has {num_secrets} "
f"password credential(s) (client secrets): {displayed}."
)
else:
report.status = "PASS"
report.status_extended = (
f"App registration {app.name} does not use password credentials."
)
findings.append(report)
return findings
@@ -90,7 +90,6 @@ class Entra(M365Service):
self._get_directory_sync_settings(),
self._get_authentication_method_configurations(),
self._get_service_principals(),
self._get_app_registrations(),
)
)
@@ -107,7 +106,6 @@ class Entra(M365Service):
str, AuthenticationMethodConfiguration
] = attributes[9]
self.service_principals: Dict[str, "ServicePrincipal"] = attributes[10]
self.app_registrations: Dict[str, "AppRegistration"] = attributes[11]
self.user_accounts_status = {}
if created_loop:
@@ -1263,59 +1261,6 @@ OAuthAppInfo
)
return service_principals
async def _get_app_registrations(self) -> Dict[str, "AppRegistration"]:
"""Retrieve application registrations from Microsoft Entra.
Fetches every application object and its password credentials (client
secrets) across all pages. Customer-owned applications should
authenticate using certificates, federated identity credentials, or
managed identities, so any entry in ``passwordCredentials`` is reported
by the related check.
Returns:
Dict[str, AppRegistration]: Application registrations keyed by the
application object ID.
"""
logger.info("Entra - Getting app registrations...")
app_registrations: Dict[str, AppRegistration] = {}
try:
app_response = await self.client.applications.get()
while app_response:
for app in getattr(app_response, "value", []) or []:
app_id = getattr(app, "app_id", None)
object_id = getattr(app, "id", None)
if not app_id or not object_id:
continue
password_credentials = []
for cred in getattr(app, "password_credentials", []) or []:
password_credentials.append(
PasswordCredential(
key_id=str(getattr(cred, "key_id", "")),
display_name=getattr(cred, "display_name", None),
start_date_time=getattr(cred, "start_date_time", None),
end_date_time=getattr(cred, "end_date_time", None),
)
)
app_registrations[object_id] = AppRegistration(
id=object_id,
app_id=app_id,
name=getattr(app, "display_name", "") or "",
password_credentials=password_credentials,
)
next_link = getattr(app_response, "odata_next_link", None)
if not next_link:
break
app_response = await self.client.applications.with_url(next_link).get()
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return app_registrations
class ConditionalAccessPolicyState(Enum):
ENABLED = "enabled"
@@ -1755,15 +1700,12 @@ class PasswordCredential(BaseModel):
Attributes:
key_id: The unique identifier of the credential.
display_name: The optional display name of the credential.
start_date_time: The time at which the credential becomes valid.
``None`` when the API does not report it.
end_date_time: The expiration time of the credential. ``None`` indicates
the secret has no recorded expiry and is treated as active.
"""
key_id: str
display_name: Optional[str] = None
start_date_time: Optional[datetime] = None
end_date_time: Optional[datetime] = None
def is_active(self, now: Optional[datetime] = None) -> bool:
@@ -1841,20 +1783,3 @@ class ServicePrincipal(BaseModel):
password_credentials: List[PasswordCredential] = []
key_credentials: List[KeyCredential] = []
directory_role_template_ids: List[str] = []
class AppRegistration(BaseModel):
"""Model representing a Microsoft Entra ID application registration.
Attributes:
id: The application object's unique identifier.
app_id: The application (client) ID.
name: The application's display name.
password_credentials: List of password credentials (client secrets)
registered on the application.
"""
id: str
app_id: str = ""
name: str = ""
password_credentials: List[PasswordCredential] = []
+1 -1
View File
@@ -120,7 +120,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">=3.10,<3.13"
version = "5.28.0"
version = "5.27.1"
[project.scripts]
prowler = "prowler.__main__:prowler"
+1 -5
View File
@@ -41,7 +41,7 @@ def set_mocked_gcp_provider(
return provider
def mock_api_client(_GCPService, service, _api_version, _):
def mock_api_client(GCPService, service, api_version, _):
client = MagicMock()
mock_api_projects_calls(client)
@@ -703,9 +703,6 @@ def mock_api_instances_calls(client: MagicMock, service: str):
"databaseVersion": "MYSQL_5_7",
"region": "us-central1",
"ipAddresses": [{"type": "PRIMARY", "ipAddress": "66.66.66.66"}],
"diskEncryptionConfiguration": {
"kmsKeyName": "projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1"
},
"settings": {
"ipConfiguration": {
"requireSsl": True,
@@ -1326,7 +1323,6 @@ def mock_api_images_calls(client: MagicMock):
client.images().list_next.return_value = None
def mock_get_image_iam_policy(project, resource):
del project
return_value = MagicMock()
if resource == "test-image-1":
return_value.execute.return_value = {
@@ -1,277 +0,0 @@
from unittest import mock
from unittest.mock import MagicMock, patch
from tests.providers.gcp.gcp_fixtures import (
GCP_EU1_LOCATION,
GCP_PROJECT_ID,
mock_is_api_active,
set_mocked_gcp_provider,
)
class Test_cloudsql_instance_cmek_encryption_enabled:
def test_no_instances(self):
cloudsql_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
new=cloudsql_client,
),
):
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
cloudsql_instance_cmek_encryption_enabled,
)
cloudsql_client.instances = []
check = cloudsql_instance_cmek_encryption_enabled()
result = check.execute()
assert len(result) == 0
def test_instance_cmek_enabled(self):
cloudsql_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
new=cloudsql_client,
),
):
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
cloudsql_instance_cmek_encryption_enabled,
)
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
Instance,
)
cloudsql_client.instances = [
Instance(
name="db-cmek",
version="POSTGRES_15",
ip_addresses=[],
region=GCP_EU1_LOCATION,
public_ip=False,
require_ssl=False,
ssl_mode="ENCRYPTED_ONLY",
automated_backups=True,
authorized_networks=[],
flags=[],
project_id=GCP_PROJECT_ID,
cmek_key_name="projects/123456789012/locations/europe-west1/keyRings/my-ring/cryptoKeys/my-key",
)
]
check = cloudsql_instance_cmek_encryption_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_id == "db-cmek"
assert result[0].location == GCP_EU1_LOCATION
assert result[0].project_id == GCP_PROJECT_ID
def test_instance_cmek_not_configured(self):
cloudsql_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
new=cloudsql_client,
),
):
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
cloudsql_instance_cmek_encryption_enabled,
)
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
Instance,
)
cloudsql_client.instances = [
Instance(
name="db-google-managed",
version="POSTGRES_15",
ip_addresses=[],
region=GCP_EU1_LOCATION,
public_ip=False,
require_ssl=False,
ssl_mode="ENCRYPTED_ONLY",
automated_backups=True,
authorized_networks=[],
flags=[],
project_id=GCP_PROJECT_ID,
cmek_key_name=None,
)
]
check = cloudsql_instance_cmek_encryption_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_id == "db-google-managed"
assert result[0].location == GCP_EU1_LOCATION
assert result[0].project_id == GCP_PROJECT_ID
def test_instance_cmek_empty_string(self):
cloudsql_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
new=cloudsql_client,
),
):
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
cloudsql_instance_cmek_encryption_enabled,
)
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
Instance,
)
cloudsql_client.instances = [
Instance(
name="db-empty-key",
version="POSTGRES_15",
ip_addresses=[],
region=GCP_EU1_LOCATION,
public_ip=False,
require_ssl=False,
ssl_mode="ENCRYPTED_ONLY",
automated_backups=True,
authorized_networks=[],
flags=[],
project_id=GCP_PROJECT_ID,
cmek_key_name="",
)
]
check = cloudsql_instance_cmek_encryption_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_id == "db-empty-key"
def test_unsupported_instance_type_skipped(self):
cloudsql_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
new=cloudsql_client,
),
):
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
cloudsql_instance_cmek_encryption_enabled,
)
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
Instance,
)
cloudsql_client.instances = [
Instance(
name="external-primary",
version="MYSQL_8_0",
ip_addresses=[],
region=GCP_EU1_LOCATION,
public_ip=False,
require_ssl=False,
ssl_mode="ENCRYPTED_ONLY",
automated_backups=False,
authorized_networks=[],
flags=[],
project_id=GCP_PROJECT_ID,
instance_type="ON_PREMISES_INSTANCE",
cmek_key_name=None,
),
Instance(
name="db-cmek",
version="POSTGRES_15",
ip_addresses=[],
region=GCP_EU1_LOCATION,
public_ip=False,
require_ssl=False,
ssl_mode="ENCRYPTED_ONLY",
automated_backups=True,
authorized_networks=[],
flags=[],
project_id=GCP_PROJECT_ID,
instance_type="CLOUD_SQL_INSTANCE",
cmek_key_name="projects/p/locations/europe-west1/keyRings/r/cryptoKeys/k",
),
]
check = cloudsql_instance_cmek_encryption_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == "db-cmek"
assert result[0].status == "PASS"
def test_service_parser_missing_disk_encryption(self):
"""Exercise the real service parser path when diskEncryptionConfiguration is absent."""
def mock_api_client_without_disk_encryption(*_args, **_kwargs):
client = MagicMock()
client.instances().list().execute.return_value = {
"items": [
{
"name": "db-no-encryption-config",
"databaseVersion": "POSTGRES_14",
"region": "us-central1",
"ipAddresses": [],
"settings": {
"ipConfiguration": {"requireSsl": True},
"backupConfiguration": {"enabled": True},
"databaseFlags": [],
},
}
]
}
client.instances().list_next.return_value = None
return client
with (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
new=mock_api_client_without_disk_encryption,
),
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]),
),
):
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
CloudSQL,
)
cloudsql_client = CloudSQL(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert len(cloudsql_client.instances) == 1
assert cloudsql_client.instances[0].cmek_key_name is None
with patch(
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
new=cloudsql_client,
):
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
cloudsql_instance_cmek_encryption_enabled,
)
check = cloudsql_instance_cmek_encryption_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_id == "db-no-encryption-config"
@@ -43,11 +43,6 @@ class TestCloudSQLService:
{"value": "test"}
]
assert cloudsql_client.instances[0].flags == []
assert cloudsql_client.instances[0].instance_type == "CLOUD_SQL_INSTANCE"
assert (
cloudsql_client.instances[0].cmek_key_name
== "projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1"
)
assert cloudsql_client.instances[0].project_id == GCP_PROJECT_ID
assert cloudsql_client.instances[1].name == "instance2"
@@ -67,14 +62,12 @@ class TestCloudSQLService:
{"value": "test"}
]
assert cloudsql_client.instances[1].flags == []
assert cloudsql_client.instances[1].instance_type == "CLOUD_SQL_INSTANCE"
assert cloudsql_client.instances[1].cmek_key_name is None
assert cloudsql_client.instances[1].project_id == GCP_PROJECT_ID
def test_instances_without_backup_configuration(self):
"""Test that CloudSQL service handles instances without backupConfiguration field"""
def mock_api_client_without_backup_config(*_args, **_kwargs):
def mock_api_client_without_backup_config(*args, **kwargs):
from unittest.mock import MagicMock
client = MagicMock()
@@ -126,7 +119,7 @@ class TestCloudSQLService:
def test_instances_with_empty_backup_configuration(self):
"""Test that CloudSQL service handles instances with empty backupConfiguration"""
def mock_api_client_with_empty_backup_config(*_args, **_kwargs):
def mock_api_client_with_empty_backup_config(*args, **kwargs):
from unittest.mock import MagicMock
client = MagicMock()
@@ -177,7 +170,7 @@ class TestCloudSQLService:
def test_instances_without_settings_fields(self):
"""Test that CloudSQL service handles instances with minimal settings"""
def mock_api_client_with_minimal_settings(*_args, **_kwargs):
def mock_api_client_with_minimal_settings(*args, **kwargs):
from unittest.mock import MagicMock
client = MagicMock()
@@ -1,282 +0,0 @@
from datetime import datetime, timedelta, timezone
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
AppRegistration,
PasswordCredential,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_entra_app_registration_client_secret_unused:
def test_no_app_registrations(self):
"""No app registrations in tenant: no findings."""
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
entra_app_registration_client_secret_unused,
)
entra_client.app_registrations = {}
check = entra_app_registration_client_secret_unused()
result = check.execute()
assert len(result) == 0
def test_app_no_password_credentials(self):
"""App with no password credentials: expected PASS."""
app_id = str(uuid4())
app_name = "Test App Clean"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
entra_app_registration_client_secret_unused,
)
entra_client.app_registrations = {
app_id: AppRegistration(
id=app_id,
app_id=str(uuid4()),
name=app_name,
password_credentials=[],
)
}
check = entra_app_registration_client_secret_unused()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"App registration {app_name} does not use password credentials."
)
assert result[0].resource_name == app_name
assert result[0].resource_id == app_id
def test_app_with_one_password_credential(self):
"""App with one password credential: expected FAIL."""
app_id = str(uuid4())
app_name = "Test App With Secret"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
entra_app_registration_client_secret_unused,
)
entra_client.app_registrations = {
app_id: AppRegistration(
id=app_id,
app_id=str(uuid4()),
name=app_name,
password_credentials=[
PasswordCredential(
key_id=str(uuid4()),
display_name="My Secret",
start_date_time="2024-01-01T00:00:00Z",
end_date_time="2025-01-01T00:00:00Z",
),
],
)
}
check = entra_app_registration_client_secret_unused()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "1 password credential(s)" in result[0].status_extended
assert "My Secret" in result[0].status_extended
assert result[0].resource_name == app_name
assert result[0].resource_id == app_id
def test_app_with_expired_password_credential_still_fails(self):
"""App with an expired password credential: still expected FAIL."""
app_id = str(uuid4())
app_name = "Legacy App"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
expired = datetime.now(timezone.utc) - timedelta(days=30)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
entra_app_registration_client_secret_unused,
)
entra_client.app_registrations = {
app_id: AppRegistration(
id=app_id,
app_id=str(uuid4()),
name=app_name,
password_credentials=[
PasswordCredential(
key_id=str(uuid4()),
display_name="old-secret",
end_date_time=expired,
),
],
)
}
check = entra_app_registration_client_secret_unused()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "1 password credential(s)" in result[0].status_extended
assert result[0].resource_name == app_name
def test_app_with_multiple_password_credentials(self):
"""App with multiple password credentials: expected FAIL."""
app_id = str(uuid4())
app_name = "Test App Multiple Secrets"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
entra_app_registration_client_secret_unused,
)
entra_client.app_registrations = {
app_id: AppRegistration(
id=app_id,
app_id=str(uuid4()),
name=app_name,
password_credentials=[
PasswordCredential(
key_id=str(uuid4()),
display_name="Secret 1",
end_date_time="2025-06-01T00:00:00Z",
),
PasswordCredential(
key_id=str(uuid4()),
display_name="Secret 2",
end_date_time="2025-12-01T00:00:00Z",
),
],
)
}
check = entra_app_registration_client_secret_unused()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "2 password credential(s)" in result[0].status_extended
assert result[0].resource_name == app_name
def test_multiple_apps_mixed(self):
"""Multiple apps: one clean, one with secrets."""
app_id_pass = str(uuid4())
app_name_pass = "Clean App"
app_id_fail = str(uuid4())
app_name_fail = "App With Secret"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
entra_app_registration_client_secret_unused,
)
entra_client.app_registrations = {
app_id_pass: AppRegistration(
id=app_id_pass,
app_id=str(uuid4()),
name=app_name_pass,
password_credentials=[],
),
app_id_fail: AppRegistration(
id=app_id_fail,
app_id=str(uuid4()),
name=app_name_fail,
password_credentials=[
PasswordCredential(
key_id=str(uuid4()),
display_name="Legacy Secret",
),
],
),
}
check = entra_app_registration_client_secret_unused()
result = check.execute()
assert len(result) == 2
result_pass = next(r for r in result if r.resource_id == app_id_pass)
result_fail = next(r for r in result if r.resource_id == app_id_fail)
assert result_pass.status == "PASS"
assert result_fail.status == "FAIL"
assert "Legacy Secret" in result_fail.status_extended