merge branch master

This commit is contained in:
pedrooot committed 2026-01-26 17:51:33 +01:00
commit 19e6daeac3
152 files changed
+10861 -1800

No files matched your search

+2 -2
View File
@@ -54,7 +54,7 @@ NEO4J_PORT=7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=neo4j_password
# Neo4j settings
NEO4J_DBMS_MAX__DATABASES=1000000
NEO4J_DBMS_MAX__DATABASES=1000
NEO4J_SERVER_MEMORY_PAGECACHE_SIZE=1G
NEO4J_SERVER_MEMORY_HEAP_INITIAL__SIZE=1G
NEO4J_SERVER_MEMORY_HEAP_MAX__SIZE=1G
@@ -66,7 +66,7 @@ NEO4J_DBMS_SECURITY_PROCEDURES_ALLOWLIST=apoc.*
NEO4J_DBMS_SECURITY_PROCEDURES_UNRESTRICTED=apoc.*
NEO4J_DBMS_CONNECTOR_BOLT_LISTEN_ADDRESS=0.0.0.0:7687
# Neo4j Prowler settings
NEO4J_INSERT_BATCH_SIZE=500
ATTACK_PATHS_FINDINGS_BATCH_SIZE=1000
# Celery-Prowler task settings
TASK_RETRY_DELAY_SECONDS=0.1
+1 -2
View File
@@ -61,9 +61,8 @@ jobs:
- name: Safety
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry run safety check --ignore 79023,79027,82754
run: poetry run safety check --ignore 79023,79027
# TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0
# TODO: 82754 filelock TOCTOU symlink vulnerability - transitive dep, awaiting upstream fix
- name: Vulture
if: steps.check-changes.outputs.any_changed == 'true'
+9 -5
View File
@@ -125,16 +125,20 @@ jobs:
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
- name: Setup pnpm and Next.js cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('ui/pnpm-lock.yaml') }}
path: |
${{ env.STORE_PATH }}
./ui/node_modules
./ui/.next/cache
key: ${{ runner.os }}-pnpm-nextjs-${{ hashFiles('ui/pnpm-lock.yaml') }}-${{ hashFiles('ui/**/*.ts', 'ui/**/*.tsx', 'ui/**/*.js', 'ui/**/*.jsx') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-nextjs-${{ hashFiles('ui/pnpm-lock.yaml') }}-
${{ runner.os }}-pnpm-nextjs-
- name: Install UI dependencies
working-directory: ./ui
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --prefer-offline
- name: Build UI application
working-directory: ./ui
run: pnpm run build
+9 -5
View File
@@ -62,18 +62,22 @@ jobs:
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
- name: Setup pnpm and Next.js cache
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('ui/pnpm-lock.yaml') }}
path: |
${{ env.STORE_PATH }}
${{ env.UI_WORKING_DIR }}/node_modules
${{ env.UI_WORKING_DIR }}/.next/cache
key: ${{ runner.os }}-pnpm-nextjs-${{ hashFiles('ui/pnpm-lock.yaml') }}-${{ hashFiles('ui/**/*.ts', 'ui/**/*.tsx', 'ui/**/*.js', 'ui/**/*.jsx') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-nextjs-${{ hashFiles('ui/pnpm-lock.yaml') }}-
${{ runner.os }}-pnpm-nextjs-
- name: Install dependencies
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run healthcheck
if: steps.check-changes.outputs.any_changed == 'true'
+10 -1
View File
@@ -20,6 +20,7 @@ Use these skills for detailed patterns on-demand:
| `playwright` | Page Object Model, MCP workflow, selectors | [SKILL.md](skills/playwright/SKILL.md) |
| `pytest` | Fixtures, mocking, markers, parametrize | [SKILL.md](skills/pytest/SKILL.md) |
| `django-drf` | ViewSets, Serializers, Filters | [SKILL.md](skills/django-drf/SKILL.md) |
| `jsonapi` | Strict JSON:API v1.1 spec compliance | [SKILL.md](skills/jsonapi/SKILL.md) |
| `zod-4` | New API (z.email(), z.uuid()) | [SKILL.md](skills/zod-4/SKILL.md) |
| `zustand-5` | Persist, selectors, slices | [SKILL.md](skills/zustand-5/SKILL.md) |
| `ai-sdk-5` | UIMessage, streaming, LangChain | [SKILL.md](skills/ai-sdk-5/SKILL.md) |
@@ -40,6 +41,7 @@ Use these skills for detailed patterns on-demand:
| `prowler-provider` | Add new cloud providers | [SKILL.md](skills/prowler-provider/SKILL.md) |
| `prowler-changelog` | Changelog entries (keepachangelog.com) | [SKILL.md](skills/prowler-changelog/SKILL.md) |
| `prowler-ci` | CI checks and PR gates (GitHub Actions) | [SKILL.md](skills/prowler-ci/SKILL.md) |
| `prowler-commit` | Professional commits (conventional-commits) | [SKILL.md](skills/prowler-commit/SKILL.md) |
| `prowler-pr` | Pull request conventions | [SKILL.md](skills/prowler-pr/SKILL.md) |
| `prowler-docs` | Documentation style guide | [SKILL.md](skills/prowler-docs/SKILL.md) |
| `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) |
@@ -51,14 +53,19 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Action | Skill |
|--------|-------|
| Add changelog entry for a PR or feature | `prowler-changelog` |
| Adding DRF pagination or permissions | `django-drf` |
| Adding new providers | `prowler-provider` |
| Adding services to existing providers | `prowler-provider` |
| After creating/modifying a skill | `skill-sync` |
| App Router / Server Actions | `nextjs-15` |
| Building AI chat features | `ai-sdk-5` |
| Committing changes | `prowler-commit` |
| Create PR that requires changelog entry | `prowler-changelog` |
| Create a PR with gh pr create | `prowler-pr` |
| Creating API endpoints | `jsonapi` |
| Creating ViewSets, serializers, or filters in api/ | `django-drf` |
| Creating Zod schemas | `zod-4` |
| Creating a git commit | `prowler-commit` |
| Creating new checks | `prowler-sdk-check` |
| Creating new skills | `skill-creator` |
| Creating/modifying Prowler UI components | `prowler-ui` |
@@ -67,14 +74,16 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Debug why a GitHub Actions job is failing | `prowler-ci` |
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
| General Prowler development questions | `prowler` |
| Generic DRF patterns | `django-drf` |
| Implementing JSON:API endpoints | `django-drf` |
| Inspect PR CI checks and gates (.github/workflows/*) | `prowler-ci` |
| Inspect PR CI workflows (.github/workflows/*): conventional-commit, pr-check-changelog, pr-conflict-checker, labeler | `prowler-pr` |
| Mapping checks to compliance controls | `prowler-compliance` |
| Mocking AWS with moto in tests | `prowler-test-sdk` |
| Modifying API responses | `jsonapi` |
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
| Reviewing compliance framework PRs | `prowler-compliance-review` |
| Testing RLS tenant isolation | `prowler-test-api` |
| Troubleshoot why a skill is missing from AGENTS.md auto-invoke | `skill-sync` |
+9 -1
View File
@@ -4,6 +4,7 @@
> - [`prowler-api`](../skills/prowler-api/SKILL.md) - Models, Serializers, Views, RLS patterns
> - [`prowler-test-api`](../skills/prowler-test-api/SKILL.md) - Testing patterns (pytest-django)
> - [`django-drf`](../skills/django-drf/SKILL.md) - Generic DRF patterns
> - [`jsonapi`](../skills/jsonapi/SKILL.md) - Strict JSON:API v1.1 spec compliance
> - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns
### Auto-invoke Skills
@@ -13,10 +14,17 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Action | Skill |
|--------|-------|
| Add changelog entry for a PR or feature | `prowler-changelog` |
| Adding DRF pagination or permissions | `django-drf` |
| Committing changes | `prowler-commit` |
| Create PR that requires changelog entry | `prowler-changelog` |
| Creating API endpoints | `jsonapi` |
| Creating ViewSets, serializers, or filters in api/ | `django-drf` |
| Creating a git commit | `prowler-commit` |
| Creating/modifying models, views, serializers | `prowler-api` |
| Generic DRF patterns | `django-drf` |
| Implementing JSON:API endpoints | `django-drf` |
| Modifying API responses | `jsonapi` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
| Testing RLS tenant isolation | `prowler-test-api` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Writing Prowler API tests | `prowler-test-api` |
+148 -70
View File
@@ -2,51 +2,79 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.18.0] (Prowler UNRELEASED)
## [1.19.0] (Prowler UNRELEASED)
### Added
- `/api/v1/overviews/compliance-watchlist` to retrieve the compliance watchlist [(#9596)](https://github.com/prowler-cloud/prowler/pull/9596)
- Support AlibabaCloud provider [(#9485)](https://github.com/prowler-cloud/prowler/pull/9485)
- `/api/v1/overviews/resource-groups` to retrieve an overview of the resource groups based on finding severities [(#9694)](https://github.com/prowler-cloud/prowler/pull/9694)
- Endpoints `GET /findings` and `GET /findings/metadata/latest` now support the `group` filter [(#9694)](https://github.com/prowler-cloud/prowler/pull/9694)
- `provider_id` and `provider_id__in` filter aliases for findings endpoints to enable consistent frontend parameter naming [(#9701)](https://github.com/prowler-cloud/prowler/pull/9701)
- Attack Paths scans for AWS providers with new Neo4j Docker service, new Celery task, new `AttackPathsScan` model and new endpoints under `/api/v1/attack-paths-scans` [(#9805)](https://github.com/prowler-cloud/prowler/pull/9805)
### 🔄 Changed
### Security
- `pyasn1` to v0.6.2 to address [CVE-2026-23490](https://nvd.nist.gov/vuln/detail/CVE-2026-23490)
- Lazy-load providers and compliance data to reduce API/worker startup memory and time [(#9857)](https://github.com/prowler-cloud/prowler/pull/9857)
---
## [1.17.2] (Prowler v5.16.2)
## [1.18.1] (Prowler v5.17.1)
### Security
- Updated dependencies to patch security vulnerabilities: Django 5.1.15 (CVE-2025-64460, CVE-2025-13372), Werkzeug 3.1.4 (CVE-2025-66221), sqlparse 0.5.5 (PVE-2025-82038), fonttools 4.60.2 (CVE-2025-66034) [(#9730)](https://github.com/prowler-cloud/prowler/pull/9730)
### 🐞 Fixed
- Improve API startup process by `manage.py` argument detection [(#9856)](https://github.com/prowler-cloud/prowler/pull/9856)
- Deleting providers don't try to delete a `None` Neo4j database when an Attack Paths scan is scheduled [(#9858)](https://github.com/prowler-cloud/prowler/pull/9858)
- Use replica database for reading Findings to add them to the Attack Paths graph [(#9861)](https://github.com/prowler-cloud/prowler/pull/9861)
- Attack paths findings loading query to use streaming generator for O(batch_size) memory instead of O(total_findings) [(#9862)](https://github.com/prowler-cloud/prowler/pull/9862)
- Lazy load Neo4j driver [(#9868)](https://github.com/prowler-cloud/prowler/pull/9868)
- Use `Findings.all_objects` to avoid the `ActiveProviderPartitionedManager` [(#9869)](https://github.com/prowler-cloud/prowler/pull/9869)
- Lazy load Neo4j driver for workers only [(#9872)](https://github.com/prowler-cloud/prowler/pull/9872)
- Improve Cypher query for inserting Findings into Attack Paths scan graphs [(#9874)](https://github.com/prowler-cloud/prowler/pull/9874)
- Clear Neo4j database cache after Attack Paths scan and each API query [(#9877)](https://github.com/prowler-cloud/prowler/pull/9877)
- Deduplicated scheduled scans for long-running providers [(#9829)](https://github.com/prowler-cloud/prowler/pull/9829)
---
## [1.18.0] (Prowler v5.17.0)
### 🚀 Added
- `/api/v1/overviews/compliance-watchlist` endpoint to retrieve the compliance watchlist [(#9596)](https://github.com/prowler-cloud/prowler/pull/9596)
- AlibabaCloud provider support [(#9485)](https://github.com/prowler-cloud/prowler/pull/9485)
- `/api/v1/overviews/resource-groups` endpoint to retrieve an overview of resource groups based on finding severities [(#9694)](https://github.com/prowler-cloud/prowler/pull/9694)
- `group` filter for `GET /findings` and `GET /findings/metadata/latest` endpoints [(#9694)](https://github.com/prowler-cloud/prowler/pull/9694)
- `provider_id` and `provider_id__in` filter aliases for findings endpoints to enable consistent frontend parameter naming [(#9701)](https://github.com/prowler-cloud/prowler/pull/9701)
- Attack Paths: `/api/v1/attack-paths-scans` for AWS providers backed by Neo4j [(#9805)](https://github.com/prowler-cloud/prowler/pull/9805)
### 🔐 Security
- Django 5.1.15 (CVE-2025-64460, CVE-2025-13372), Werkzeug 3.1.4 (CVE-2025-66221), sqlparse 0.5.5 (PVE-2025-82038), fonttools 4.60.2 (CVE-2025-66034) [(#9730)](https://github.com/prowler-cloud/prowler/pull/9730)
- `safety` to `3.7.0` and `filelock` to `3.20.3` due to [Safety vulnerability 82754 (CVE-2025-68146)](https://data.safetycli.com/v/82754/97c/) [(#9816)](https://github.com/prowler-cloud/prowler/pull/9816)
- `pyasn1` to v0.6.2 to address [CVE-2026-23490](https://nvd.nist.gov/vuln/detail/CVE-2026-23490) [(#9818)](https://github.com/prowler-cloud/prowler/pull/9818)
- `django-allauth[saml]` to v65.13.0 to address [CVE-2025-65431](https://nvd.nist.gov/vuln/detail/CVE-2025-65431) [(#9575)](https://github.com/prowler-cloud/prowler/pull/9575)
---
## [1.17.1] (Prowler v5.16.1)
### Changed
### 🔄 Changed
- Security Hub integration error when no regions [(#9635)](https://github.com/prowler-cloud/prowler/pull/9635)
### Fixed
### 🐞 Fixed
- Orphan scheduled scans caused by transaction isolation during provider creation [(#9633)](https://github.com/prowler-cloud/prowler/pull/9633)
---
## [1.17.0] (Prowler v5.16.0)
### Added
### 🚀 Added
- New endpoint to retrieve and overview of the categories based on finding severities [(#9529)](https://github.com/prowler-cloud/prowler/pull/9529)
- Endpoints `GET /findings` and `GET /findings/latests` can now use the category filter [(#9529)](https://github.com/prowler-cloud/prowler/pull/9529)
- Account id, alias and provider name to PDF reporting table [(#9574)](https://github.com/prowler-cloud/prowler/pull/9574)
### Changed
### 🔄 Changed
- Endpoint `GET /overviews/attack-surfaces` no longer returns the related check IDs [(#9529)](https://github.com/prowler-cloud/prowler/pull/9529)
- OpenAI provider to only load chat-compatible models with tool calling support [(#9523)](https://github.com/prowler-cloud/prowler/pull/9523)
- Increased execution delay for the first scheduled scan tasks to 5 seconds[(#9558)](https://github.com/prowler-cloud/prowler/pull/9558)
### Fixed
### 🐞 Fixed
- Made `scan_id` a required filter in the compliance overview endpoint [(#9560)](https://github.com/prowler-cloud/prowler/pull/9560)
- Reduced unnecessary UPDATE resources operations by only saving when tag mappings change, lowering write load during scans [(#9569)](https://github.com/prowler-cloud/prowler/pull/9569)
@@ -54,19 +82,22 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.16.1] (Prowler v5.15.1)
### Fixed
### 🐞 Fixed
- Race condition in scheduled scan creation by adding countdown to task [(#9516)](https://github.com/prowler-cloud/prowler/pull/9516)
## [1.16.0] (Prowler v5.15.0)
### Added
### 🚀 Added
- New endpoint to retrieve an overview of the attack surfaces [(#9309)](https://github.com/prowler-cloud/prowler/pull/9309)
- New endpoint `GET /api/v1/overviews/findings_severity/timeseries` to retrieve daily aggregated findings by severity level [(#9363)](https://github.com/prowler-cloud/prowler/pull/9363)
- Lighthouse AI support for Amazon Bedrock API key [(#9343)](https://github.com/prowler-cloud/prowler/pull/9343)
- Exception handler for provider deletions during scans [(#9414)](https://github.com/prowler-cloud/prowler/pull/9414)
- Support to use admin credentials through the read replica database [(#9440)](https://github.com/prowler-cloud/prowler/pull/9440)
### Changed
### 🔄 Changed
- Error messages from Lighthouse celery tasks [(#9165)](https://github.com/prowler-cloud/prowler/pull/9165)
- Restore the compliance overview endpoint's mandatory filters [(#9338)](https://github.com/prowler-cloud/prowler/pull/9338)
@@ -74,7 +105,8 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.15.2] (Prowler v5.14.2)
### Fixed
### 🐞 Fixed
- Unique constraint violation during compliance overviews task [(#9436)](https://github.com/prowler-cloud/prowler/pull/9436)
- Division by zero error in ENS PDF report when all requirements are manual [(#9443)](https://github.com/prowler-cloud/prowler/pull/9443)
@@ -82,7 +114,8 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.15.1] (Prowler v5.14.1)
### Fixed
### 🐞 Fixed
- Fix typo in PDF reporting [(#9345)](https://github.com/prowler-cloud/prowler/pull/9345)
- Fix IaC provider initialization failure when mutelist processor is configured [(#9331)](https://github.com/prowler-cloud/prowler/pull/9331)
- Match logic for ThreatScore when counting findings [(#9348)](https://github.com/prowler-cloud/prowler/pull/9348)
@@ -91,7 +124,8 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.15.0] (Prowler v5.14.0)
### Added
### 🚀 Added
- IaC (Infrastructure as Code) provider support for remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
- Extend `GET /api/v1/providers` with provider-type filters and optional pagination disable to support the new Overview filters [(#8975)](https://github.com/prowler-cloud/prowler/pull/8975)
- New endpoint to retrieve the number of providers grouped by provider type [(#8975)](https://github.com/prowler-cloud/prowler/pull/8975)
@@ -110,11 +144,13 @@ All notable changes to the **Prowler API** are documented in this file.
- Enhanced compliance overview endpoint with provider filtering and latest scan aggregation [(#9244)](https://github.com/prowler-cloud/prowler/pull/9244)
- New endpoint `GET /api/v1/overview/regions` to retrieve aggregated findings data by region [(#9273)](https://github.com/prowler-cloud/prowler/pull/9273)
### Changed
### 🔄 Changed
- Optimized database write queries for scan related tasks [(#9190)](https://github.com/prowler-cloud/prowler/pull/9190)
- Date filters are now optional for `GET /api/v1/overviews/services` endpoint; returns latest scan data by default [(#9248)](https://github.com/prowler-cloud/prowler/pull/9248)
### Fixed
### 🐞 Fixed
- Scans no longer fail when findings have UIDs exceeding 300 characters; such findings are now skipped with detailed logging [(#9246)](https://github.com/prowler-cloud/prowler/pull/9246)
- Updated unique constraint for `Provider` model to exclude soft-deleted entries, resolving duplicate errors when re-deleting providers [(#9054)](https://github.com/prowler-cloud/prowler/pull/9054)
- Removed compliance generation for providers without compliance frameworks [(#9208)](https://github.com/prowler-cloud/prowler/pull/9208)
@@ -122,14 +158,16 @@ All notable changes to the **Prowler API** are documented in this file.
- Severity overview endpoint now ignores muted findings as expected [(#9283)](https://github.com/prowler-cloud/prowler/pull/9283)
- Fixed discrepancy between ThreatScore PDF report values and database calculations [(#9296)](https://github.com/prowler-cloud/prowler/pull/9296)
### Security
### 🔐 Security
- Django updated to the latest 5.1 security release, 5.1.14, due to problems with potential [SQL injection](https://github.com/prowler-cloud/prowler/security/dependabot/113) and [denial-of-service vulnerability](https://github.com/prowler-cloud/prowler/security/dependabot/114) [(#9176)](https://github.com/prowler-cloud/prowler/pull/9176)
---
## [1.14.1] (Prowler v5.13.1)
### Fixed
### 🐞 Fixed
- `/api/v1/overviews/providers` collapses data by provider type so the UI receives a single aggregated record per cloud family even when multiple accounts exist [(#9053)](https://github.com/prowler-cloud/prowler/pull/9053)
- Added retry logic to database transactions to handle Aurora read replica connection failures during scale-down events [(#9064)](https://github.com/prowler-cloud/prowler/pull/9064)
- Security Hub integrations stop failing when they read relationships via the replica by allowing replica relations and saving updates through the primary [(#9080)](https://github.com/prowler-cloud/prowler/pull/9080)
@@ -138,7 +176,8 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.14.0] (Prowler v5.13.0)
### Added
### 🚀 Added
- Default JWT keys are generated and stored if they are missing from configuration [(#8655)](https://github.com/prowler-cloud/prowler/pull/8655)
- `compliance_name` for each compliance [(#7920)](https://github.com/prowler-cloud/prowler/pull/7920)
- Support C5 compliance framework for the AWS provider [(#8830)](https://github.com/prowler-cloud/prowler/pull/8830)
@@ -151,35 +190,41 @@ All notable changes to the **Prowler API** are documented in this file.
- Support Common Cloud Controls for AWS, Azure and GCP [(#8000)](https://github.com/prowler-cloud/prowler/pull/8000)
- Add `provider_id__in` filter support to findings and findings severity overview endpoints [(#8951)](https://github.com/prowler-cloud/prowler/pull/8951)
### Changed
### 🔄 Changed
- Now the MANAGE_ACCOUNT permission is required to modify or read user permissions instead of MANAGE_USERS [(#8281)](https://github.com/prowler-cloud/prowler/pull/8281)
- Now at least one user with MANAGE_ACCOUNT permission is required in the tenant [(#8729)](https://github.com/prowler-cloud/prowler/pull/8729)
### Security
### 🔐 Security
- Django updated to the latest 5.1 security release, 5.1.13, due to problems with potential [SQL injection](https://github.com/prowler-cloud/prowler/security/dependabot/104) and [directory traversals](https://github.com/prowler-cloud/prowler/security/dependabot/103) [(#8842)](https://github.com/prowler-cloud/prowler/pull/8842)
---
## [1.13.2] (Prowler v5.12.3)
### Fixed
### 🐞 Fixed
- 500 error when deleting user [(#8731)](https://github.com/prowler-cloud/prowler/pull/8731)
---
## [1.13.1] (Prowler v5.12.2)
### Changed
### 🔄 Changed
- Renamed compliance overview task queue to `compliance` [(#8755)](https://github.com/prowler-cloud/prowler/pull/8755)
### Security
### 🔐 Security
- Django updated to the latest 5.1 security release, 5.1.12, due to [problems](https://www.djangoproject.com/weblog/2025/sep/03/security-releases/) with potential SQL injection in FilteredRelation column aliases [(#8693)](https://github.com/prowler-cloud/prowler/pull/8693)
---
## [1.13.0] (Prowler v5.12.0)
### Added
### 🚀 Added
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
- `GET /overviews/findings_severity` now supports `filter[status]` and `filter[status__in]` to aggregate by specific statuses (`FAIL`, `PASS`)[(#8186)](https://github.com/prowler-cloud/prowler/pull/8186)
- Throttling options for `/api/v1/tokens` using the `DJANGO_THROTTLE_TOKEN_OBTAIN` environment variable [(#8647)](https://github.com/prowler-cloud/prowler/pull/8647)
@@ -188,101 +233,120 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.12.0] (Prowler v5.11.0)
### Added
### 🚀 Added
- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527)
- Integration with Amazon Security Hub, enabling sending findings to Security Hub [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365)
- Generate ASFF output for AWS providers with SecurityHub integration enabled [(#8569)](https://github.com/prowler-cloud/prowler/pull/8569)
### Fixed
### 🐞 Fixed
- GitHub provider always scans user instead of organization when using provider UID [(#8587)](https://github.com/prowler-cloud/prowler/pull/8587)
---
## [1.11.0] (Prowler v5.10.0)
### Added
### 🚀 Added
- Github provider support [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271)
- Integration with Amazon S3, enabling storage and retrieval of scan data via S3 buckets [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056)
### Fixed
### 🐞 Fixed
- Avoid sending errors to Sentry in M365 provider when user authentication fails [(#8420)](https://github.com/prowler-cloud/prowler/pull/8420)
---
## [1.10.2] (Prowler v5.9.2)
### Changed
### 🔄 Changed
- Optimized queries for resources views [(#8336)](https://github.com/prowler-cloud/prowler/pull/8336)
---
## [v1.10.1] (Prowler v5.9.1)
### Fixed
### 🐞 Fixed
- Calculate failed findings during scans to prevent heavy database queries [(#8322)](https://github.com/prowler-cloud/prowler/pull/8322)
---
## [v1.10.0] (Prowler v5.9.0)
### Added
### 🚀 Added
- SSO with SAML support [(#8175)](https://github.com/prowler-cloud/prowler/pull/8175)
- `GET /resources/metadata`, `GET /resources/metadata/latest` and `GET /resources/latest` to expose resource metadata and latest scan results [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
### Changed
### 🔄 Changed
- `/processors` endpoints to post-process findings. Currently, only the Mutelist processor is supported to allow to mute findings.
- Optimized the underlying queries for resources endpoints [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- Optimized include parameters for resources view [(#8229)](https://github.com/prowler-cloud/prowler/pull/8229)
- Optimized overview background tasks [(#8300)](https://github.com/prowler-cloud/prowler/pull/8300)
### Fixed
### 🐞 Fixed
- Search filter for findings and resources [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- RBAC is now applied to `GET /overviews/providers` [(#8277)](https://github.com/prowler-cloud/prowler/pull/8277)
### Changed
### 🔄 Changed
- `POST /schedules/daily` returns a `409 CONFLICT` if already created [(#8258)](https://github.com/prowler-cloud/prowler/pull/8258)
### Security
### 🔐 Security
- Enhanced password validation to enforce 12+ character passwords with special characters, uppercase, lowercase, and numbers [(#8225)](https://github.com/prowler-cloud/prowler/pull/8225)
---
## [v1.9.1] (Prowler v5.8.1)
### Added
### 🚀 Added
- Custom exception for provider connection errors during scans [(#8234)](https://github.com/prowler-cloud/prowler/pull/8234)
### Changed
### 🔄 Changed
- Summary and overview tasks now use a dedicated queue and no longer propagate errors to compliance tasks [(#8214)](https://github.com/prowler-cloud/prowler/pull/8214)
### Fixed
### 🐞 Fixed
- Scan with no resources will not trigger legacy code for findings metadata [(#8183)](https://github.com/prowler-cloud/prowler/pull/8183)
- Invitation email comparison case-insensitive [(#8206)](https://github.com/prowler-cloud/prowler/pull/8206)
### Removed
### ❌ Removed
- Validation of the provider's secret type during updates [(#8197)](https://github.com/prowler-cloud/prowler/pull/8197)
---
## [v1.9.0] (Prowler v5.8.0)
### Added
### 🚀 Added
- Support GCP Service Account key [(#7824)](https://github.com/prowler-cloud/prowler/pull/7824)
- `GET /compliance-overviews` endpoints to retrieve compliance metadata and specific requirements statuses [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877)
- Lighthouse configuration support [(#7848)](https://github.com/prowler-cloud/prowler/pull/7848)
### Changed
### 🔄 Changed
- Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877)
- Optional `user` and `password` for M365 provider [(#7992)](https://github.com/prowler-cloud/prowler/pull/7992)
### Fixed
### 🐞 Fixed
- Scheduled scans are no longer deleted when their daily schedule run is disabled [(#8082)](https://github.com/prowler-cloud/prowler/pull/8082)
---
## [v1.8.5] (Prowler v5.7.5)
### Fixed
### 🐞 Fixed
- Normalize provider UID to ensure safe and unique export directory paths [(#8007)](https://github.com/prowler-cloud/prowler/pull/8007).
- Blank resource types in `/metadata` endpoints [(#8027)](https://github.com/prowler-cloud/prowler/pull/8027)
@@ -290,20 +354,24 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.8.4] (Prowler v5.7.4)
### Removed
### ❌ Removed
- Reverted RLS transaction handling and DB custom backend [(#7994)](https://github.com/prowler-cloud/prowler/pull/7994)
---
## [v1.8.3] (Prowler v5.7.3)
### Added
### 🚀 Added
- Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935)
### Changed
### 🔄 Changed
- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
### Fixed
### 🐞 Fixed
- Transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916)
- Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932)
@@ -311,7 +379,8 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.8.2] (Prowler v5.7.2)
### Fixed
### 🐞 Fixed
- Task lookup to use task_kwargs instead of task_args for scan report resolution [(#7830)](https://github.com/prowler-cloud/prowler/pull/7830)
- Kubernetes UID validation to allow valid context names [(#7871)](https://github.com/prowler-cloud/prowler/pull/7871)
- Connection status verification before launching a scan [(#7831)](https://github.com/prowler-cloud/prowler/pull/7831)
@@ -322,14 +391,16 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.8.1] (Prowler v5.7.1)
### Fixed
### 🐞 Fixed
- Added database index to improve performance on finding lookup [(#7800)](https://github.com/prowler-cloud/prowler/pull/7800)
---
## [v1.8.0] (Prowler v5.7.0)
### Added
### 🚀 Added
- Huge improvements to `/findings/metadata` and resource related filters for findings [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690)
- Improvements to `/overviews` endpoints [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690)
- Queue to perform backfill background tasks [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690)
@@ -340,7 +411,7 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.7.0] (Prowler v5.6.0)
### Added
### 🚀 Added
- M365 as a new provider [(#7563)](https://github.com/prowler-cloud/prowler/pull/7563)
- `compliance/` folder and ZIP‐export functionality for all compliance reports [(#7653)](https://github.com/prowler-cloud/prowler/pull/7653)
@@ -350,7 +421,7 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.6.0] (Prowler v5.5.0)
### Added
### 🚀 Added
- Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167)
- HTTP Security Headers [(#7289)](https://github.com/prowler-cloud/prowler/pull/7289)
@@ -362,14 +433,16 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.5.4] (Prowler v5.4.4)
### Fixed
### 🐞 Fixed
- Bug with periodic tasks when trying to delete a provider [(#7466)](https://github.com/prowler-cloud/prowler/pull/7466)
---
## [v1.5.3] (Prowler v5.4.3)
### Fixed
### 🐞 Fixed
- Duplicated scheduled scans handling [(#7401)](https://github.com/prowler-cloud/prowler/pull/7401)
- Environment variable to configure the deletion task batch size [(#7423)](https://github.com/prowler-cloud/prowler/pull/7423)
@@ -377,14 +450,16 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.5.2] (Prowler v5.4.2)
### Changed
### 🔄 Changed
- Refactored deletion logic and implemented retry mechanism for deletion tasks [(#7349)](https://github.com/prowler-cloud/prowler/pull/7349)
---
## [v1.5.1] (Prowler v5.4.1)
### Fixed
### 🐞 Fixed
- Handle response in case local files are missing [(#7183)](https://github.com/prowler-cloud/prowler/pull/7183)
- Race condition when deleting export files after the S3 upload [(#7172)](https://github.com/prowler-cloud/prowler/pull/7172)
- Handle exception when a provider has no secret in test connection [(#7283)](https://github.com/prowler-cloud/prowler/pull/7283)
@@ -393,19 +468,22 @@ All notable changes to the **Prowler API** are documented in this file.
## [v1.5.0] (Prowler v5.4.0)
### Added
### 🚀 Added
- Social login integration with Google and GitHub [(#6906)](https://github.com/prowler-cloud/prowler/pull/6906)
- API scan report system, now all scans launched from the API will generate a compressed file with the report in OCSF, CSV and HTML formats [(#6878)](https://github.com/prowler-cloud/prowler/pull/6878)
- Configurable Sentry integration [(#6874)](https://github.com/prowler-cloud/prowler/pull/6874)
### Changed
### 🔄 Changed
- Optimized `GET /findings` endpoint to improve response time and size [(#7019)](https://github.com/prowler-cloud/prowler/pull/7019)
---
## [v1.4.0] (Prowler v5.3.0)
### Changed
### 🔄 Changed
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700)
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800)
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863)
+1 -1
View File
@@ -32,7 +32,7 @@ start_prod_server() {
start_worker() {
echo "Starting the worker..."
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill,overview,integrations,compliance -E --max-tasks-per-child 1
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill,overview,integrations,compliance,attack-paths-scans -E --max-tasks-per-child 1
}
start_worker_beat() {
+337 -186
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -821,7 +821,7 @@ version = "4.10.0"
description = "High-level concurrency and networking framework on top of asyncio or Trio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"},
{file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"},
@@ -2676,13 +2676,14 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-allauth"
version = "65.11.0"
version = "65.13.0"
description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "django_allauth-65.11.0.tar.gz", hash = "sha256:d08ee0b60a1a54f84720bb749518628c517c9af40b6cfb3bc980206e182745ab"},
{file = "django_allauth-65.13.0-py3-none-any.whl", hash = "sha256:119c0cf1cc2e0d1a0fe2f13588f30951d64989256084de2d60f13ab9308f9fa0"},
{file = "django_allauth-65.13.0.tar.gz", hash = "sha256:7d7b7e7ad603eb3864c142f051e2cce7be2f9a9c6945a51172ec83d48c6c843b"},
]
[package.dependencies]
@@ -2694,6 +2695,7 @@ python3-saml = {version = ">=1.15.0,<2.0.0", optional = true, markers = "extra =
requests = {version = ">=2.0.0,<3", optional = true, markers = "extra == \"socialaccount\""}
[package.extras]
headless = ["pyjwt[crypto] (>=2.0,<3)"]
headless-spec = ["PyYAML (>=6,<7)"]
idp-oidc = ["oauthlib (>=3.3.0,<4)", "pyjwt[crypto] (>=2.0,<3)"]
mfa = ["fido2 (>=1.1.2,<3)", "qrcode (>=7.0.0,<9)"]
@@ -3188,21 +3190,16 @@ testing = ["hatch", "pre-commit", "pytest", "tox"]
[[package]]
name = "filelock"
version = "3.12.4"
version = "3.20.3"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
{file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
{file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
{file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
typing = ["typing-extensions (>=4.7.1) ; python_version < \"3.11\""]
[[package]]
name = "flask"
version = "3.1.2"
@@ -3948,7 +3945,7 @@ version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
@@ -3988,7 +3985,7 @@ version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
@@ -4025,7 +4022,7 @@ version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
@@ -4303,6 +4300,18 @@ files = [
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
[[package]]
name = "joblib"
version = "1.5.3"
description = "Lightweight pipelining with Python functions"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"},
{file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"},
]
[[package]]
name = "jsonpickle"
version = "4.1.1"
@@ -5501,6 +5510,32 @@ files = [
{file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
]
[[package]]
name = "nltk"
version = "3.9.2"
description = "Natural Language Toolkit"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a"},
{file = "nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419"},
]
[package.dependencies]
click = "*"
joblib = "*"
regex = ">=2021.8.3"
tqdm = "*"
[package.extras]
all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"]
corenlp = ["requests"]
machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"]
plot = ["matplotlib"]
tgrep = ["pyparsing"]
twitter = ["twython"]
[[package]]
name = "numpy"
version = "2.0.2"
@@ -6304,33 +6339,34 @@ resolved_reference = "2c4f866e42c25973d6eb3dc7d58e7ce72b1ed3f0"
[[package]]
name = "psutil"
version = "6.0.0"
version = "6.1.1"
description = "Cross-platform lib for process and system monitoring in Python."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["main", "dev"]
groups = ["main"]
files = [
{file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"},
{file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"},
{file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"},
{file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"},
{file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"},
{file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"},
{file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"},
{file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"},
{file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"},
{file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"},
{file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"},
{file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"},
{file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"},
{file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"},
{file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"},
{file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"},
{file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"},
{file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"},
{file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"},
{file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"},
{file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"},
{file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"},
{file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"},
{file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"},
{file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"},
{file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"},
{file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"},
{file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"},
{file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"},
{file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"},
{file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"},
]
[package.extras]
test = ["enum34 ; python_version <= \"3.4\"", "ipaddress ; python_version < \"3.0\"", "mock ; python_version < \"3.0\"", "pywin32 ; sys_platform == \"win32\"", "wmi ; sys_platform == \"win32\""]
dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"]
test = ["pytest", "pytest-xdist", "setuptools"]
[[package]]
name = "psycopg2-binary"
@@ -6554,133 +6590,122 @@ files = [
[[package]]
name = "pydantic"
version = "2.11.7"
version = "2.9.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"},
{file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"},
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.33.2"
typing-extensions = ">=4.12.2"
typing-inspection = ">=0.4.0"
pydantic-core = "2.23.4"
typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
[package.extras]
email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""]
[[package]]
name = "pydantic-core"
version = "2.33.2"
version = "2.23.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
{file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
{file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
{file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
{file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
{file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
{file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
{file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
{file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
{file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
{file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
{file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
{file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
{file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
{file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
{file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
{file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
{file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
{file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
{file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
{file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
{file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
{file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
{file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
{file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
{file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
{file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
{file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
{file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
{file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
{file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
{file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
{file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
{file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
{file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
{file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
{file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
{file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
{file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
]
[package.dependencies]
@@ -7301,6 +7326,147 @@ attrs = ">=22.2.0"
rpds-py = ">=0.7.0"
typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""}
[[package]]
name = "regex"
version = "2026.1.15"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"},
{file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"},
{file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"},
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"},
{file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"},
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"},
{file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"},
{file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"},
{file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"},
{file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"},
{file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"},
{file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"},
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"},
{file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"},
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"},
{file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"},
{file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"},
{file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"},
{file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"},
{file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"},
{file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"},
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"},
{file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"},
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"},
{file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"},
{file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"},
{file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"},
{file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"},
{file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"},
{file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"},
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"},
{file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"},
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"},
{file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"},
{file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"},
{file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"},
{file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"},
{file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"},
{file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"},
{file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"},
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"},
{file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"},
{file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"},
{file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"},
{file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"},
{file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"},
{file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"},
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"},
{file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"},
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"},
{file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"},
{file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"},
{file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"},
{file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"},
{file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"},
{file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"},
{file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"},
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"},
{file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"},
{file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"},
{file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"},
{file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"},
{file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"},
{file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"},
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"},
{file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"},
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"},
{file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"},
{file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"},
{file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"},
{file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"},
]
[[package]]
name = "reportlab"
version = "4.4.4"
@@ -7716,34 +7882,34 @@ crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
[[package]]
name = "safety"
version = "3.2.9"
description = "Checks installed dependencies for known vulnerabilities and licenses."
version = "3.7.0"
description = "Scan dependencies for known vulnerabilities and licenses."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "safety-3.2.9-py3-none-any.whl", hash = "sha256:5e199c057550dc6146c081084274279dfb98c17735193b028db09a55ea508f1a"},
{file = "safety-3.2.9.tar.gz", hash = "sha256:494bea752366161ac9e0742033d2a82e4dc51d7c788be42e0ecf5f3ef36b8071"},
{file = "safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf"},
{file = "safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e"},
]
[package.dependencies]
Authlib = ">=1.2.0"
Click = ">=8.0.2"
dparse = ">=0.6.4b0"
filelock = ">=3.12.2,<3.13.0"
authlib = ">=1.2.0"
click = ">=8.0.2"
dparse = ">=0.6.4"
filelock = ">=3.16.1,<4.0"
httpx = "*"
jinja2 = ">=3.1.0"
marshmallow = ">=3.15.0"
nltk = ">=3.9"
packaging = ">=21.0"
psutil = ">=6.0.0,<6.1.0"
pydantic = ">=1.10.12"
pydantic = ">=2.6.0"
requests = "*"
rich = "*"
"ruamel.yaml" = ">=0.17.21"
safety-schemas = ">=0.0.4"
setuptools = ">=65.5.1"
typer = "*"
ruamel-yaml = ">=0.17.21"
safety-schemas = "0.0.16"
tenacity = ">=8.1.0"
tomlkit = "*"
typer = ">=0.16.0"
typing-extensions = ">=4.7.1"
urllib3 = ">=1.26.5"
[package.extras]
github = ["pygithub (>=1.43.3)"]
@@ -7752,20 +7918,20 @@ spdx = ["spdx-tools (>=0.8.2)"]
[[package]]
name = "safety-schemas"
version = "0.0.5"
version = "0.0.16"
description = "Schemas for Safety tools"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "safety_schemas-0.0.5-py3-none-any.whl", hash = "sha256:6ac9eb71e60f0d4e944597c01dd48d6d8cd3d467c94da4aba3702a05a3a6ab4f"},
{file = "safety_schemas-0.0.5.tar.gz", hash = "sha256:0de5fc9a53d4423644a8ce9a17a2e474714aa27e57f3506146e95a41710ff104"},
{file = "safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44"},
{file = "safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e"},
]
[package.dependencies]
dparse = ">=0.6.4b0"
dparse = ">=0.6.4"
packaging = ">=21.0"
pydantic = "*"
pydantic = ">=2.6.0"
ruamel-yaml = ">=0.17.21"
typing-extensions = ">=4.7.1"
@@ -7959,7 +8125,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -8041,7 +8207,7 @@ version = "9.1.2"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.9"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"},
{file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"},
@@ -8152,21 +8318,6 @@ files = [
{file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"},
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[[package]]
name = "tzdata"
version = "2025.2"
@@ -8878,4 +9029,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
content-hash = "87a29ee9d43486c2769aedae09b8f37b1f3709b62d39d602962307072e4d71ab"
content-hash = "15b849034c0969b944c45f341169a42160efe2525fe60a4cadd8a8909202e9e3"
+5 -4
View File
@@ -8,7 +8,7 @@ dependencies = [
"celery[pytest] (>=5.4.0,<6.0.0)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
"django (==5.1.15)",
"django-allauth[saml] (>=65.8.0,<66.0.0)",
"django-allauth[saml] (>=65.13.0,<66.0.0)",
"django-celery-beat (>=2.7.0,<3.0.0)",
"django-celery-results (>=2.5.1,<3.0.0)",
"django-cors-headers==4.4.0",
@@ -49,7 +49,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.18.0"
version = "1.19.0"
[project.scripts]
celery = "src.backend.config.settings.celery"
@@ -70,6 +70,7 @@ pytest-env = "1.1.3"
pytest-randomly = "3.15.0"
pytest-xdist = "3.6.1"
ruff = "0.5.0"
safety = "3.2.9"
tqdm = "4.67.1"
safety = "3.7.0"
filelock = "3.20.3"
vulture = "2.14"
tqdm = "4.67.1"
+34 -8
View File
@@ -1,5 +1,4 @@
import logging
import atexit
import os
import sys
from pathlib import Path
@@ -32,20 +31,47 @@ class ApiConfig(AppConfig):
from api import schema_extensions # noqa: F401
from api import signals # noqa: F401
from api.attack_paths import database as graph_database
from api.compliance import load_prowler_compliance
# Generate required cryptographic keys if not present, but only if:
# `"manage.py" not in sys.argv`: If an external server (e.g., Gunicorn) is running the app
# `"manage.py" not in sys.argv[0]`: If an external server (e.g., Gunicorn) is running the app
# `os.environ.get("RUN_MAIN")`: If it's not a Django command or using `runserver`,
# only the main process will do it
if "manage.py" not in sys.argv or os.environ.get("RUN_MAIN"):
if (len(sys.argv) >= 1 and "manage.py" not in sys.argv[0]) or os.environ.get(
"RUN_MAIN"
):
self._ensure_crypto_keys()
if not getattr(settings, "TESTING", False):
graph_database.init_driver()
atexit.register(graph_database.close_driver)
# Commands that don't need Neo4j
SKIP_NEO4J_DJANGO_COMMANDS = [
"makemigrations",
"migrate",
"pgpartition",
"check",
"help",
"showmigrations",
"check_and_fix_socialaccount_sites_migration",
]
load_prowler_compliance()
# Skip Neo4j initialization during tests, some Django commands, and Celery
if getattr(settings, "TESTING", False) or (
len(sys.argv) > 1
and (
(
"manage.py" in sys.argv[0]
and sys.argv[1] in SKIP_NEO4J_DJANGO_COMMANDS
)
or "celery" in sys.argv[0]
)
):
logger.info(
"Skipping Neo4j initialization because tests, some Django commands or Celery"
)
else:
graph_database.init_driver()
# Neo4j driver is initialized at API startup (see api.attack_paths.database)
# It remains lazy for Celery workers and selected Django commands
def _ensure_crypto_keys(self):
"""
+21 -4
View File
@@ -1,13 +1,12 @@
import atexit
import logging
import threading
from contextlib import contextmanager
from typing import Iterator
from uuid import UUID
import neo4j
import neo4j.exceptions
from django.conf import settings
from api.attack_paths.retryable_session import RetryableSession
@@ -42,10 +41,18 @@ def init_driver() -> neo4j.Driver:
config = settings.DATABASES["neo4j"]
_driver = neo4j.GraphDatabase.driver(
uri, auth=(config["USER"], config["PASSWORD"])
uri,
auth=(config["USER"], config["PASSWORD"]),
keep_alive=True,
max_connection_lifetime=7200,
connection_acquisition_timeout=120,
max_connection_pool_size=50,
)
_driver.verify_connectivity()
# Register cleanup handler (only runs once since we're inside the _driver is None block)
atexit.register(close_driver)
return _driver
@@ -71,7 +78,6 @@ def get_session(database: str | None = None) -> Iterator[RetryableSession]:
try:
session_wrapper = RetryableSession(
session_factory=lambda: get_driver().session(database=database),
close_driver=close_driver, # Just to avoid circular imports
max_retries=SERVICE_UNAVAILABLE_MAX_RETRIES,
)
yield session_wrapper
@@ -119,6 +125,17 @@ def drop_subgraph(database: str, root_node_label: str, root_node_id: str) -> int
return 0 # As there are no nodes to delete, the result is empty
def clear_cache(database: str) -> None:
query = "CALL db.clearQueryCaches()"
try:
with get_session(database) as session:
session.run(query)
except GraphDatabaseQueryException as exc:
logging.warning(f"Failed to clear query cache for database `{database}`: {exc}")
# Neo4j functions related to Prowler + Cartography
DATABASE_NAME_TEMPLATE = "db-{attack_paths_scan_id}"
@@ -17,11 +17,9 @@ class RetryableSession:
def __init__(
self,
session_factory: Callable[[], neo4j.Session],
close_driver: Callable[[], None], # Just to avoid circular imports
max_retries: int,
) -> None:
self._session_factory = session_factory
self._close_driver = close_driver
self._max_retries = max(0, max_retries)
self._session = self._session_factory()
@@ -58,7 +56,7 @@ class RetryableSession:
def _call_with_retry(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
attempt = 0
last_exc: neo4j.exceptions.ServiceUnavailable | None = None
last_exc: Exception | None = None
while attempt <= self._max_retries:
try:
@@ -66,7 +64,9 @@ class RetryableSession:
return method(*args, **kwargs)
except (
neo4j.exceptions.ServiceUnavailable
BrokenPipeError,
ConnectionResetError,
neo4j.exceptions.ServiceUnavailable,
) as exc: # pragma: no cover - depends on infra
last_exc = exc
attempt += 1
@@ -75,7 +75,7 @@ class RetryableSession:
raise
logger.warning(
f"Neo4j session {method_name} failed with ServiceUnavailable ({attempt}/{self._max_retries} attempts). Retrying..."
f"Neo4j session {method_name} failed with {type(exc).__name__} ({attempt}/{self._max_retries} attempts). Retrying..."
)
self._refresh_session()
@@ -83,7 +83,10 @@ class RetryableSession:
def _refresh_session(self) -> None:
if self._session is not None:
self._session.close()
try:
self._session.close()
except Exception:
# Best-effort close; failures just mean we open a new session below
pass
self._close_driver()
self._session = self._session_factory()
+133 -32
View File
@@ -1,15 +1,99 @@
from types import MappingProxyType
from collections.abc import Iterable, Mapping
from api.models import Provider
from prowler.config.config import get_available_compliance_frameworks
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.check.models import CheckMetadata
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE = {}
PROWLER_CHECKS = {}
AVAILABLE_COMPLIANCE_FRAMEWORKS = {}
class LazyComplianceTemplate(Mapping):
"""Lazy-load compliance templates per provider on first access."""
def __init__(self, provider_types: Iterable[str] | None = None) -> None:
if provider_types is None:
provider_types = Provider.ProviderChoices.values
self._provider_types = tuple(provider_types)
self._provider_types_set = set(self._provider_types)
self._cache: dict[str, dict] = {}
def _load_provider(self, provider_type: str) -> dict:
if provider_type not in self._provider_types_set:
raise KeyError(provider_type)
cached = self._cache.get(provider_type)
if cached is not None:
return cached
_ensure_provider_loaded(provider_type)
return self._cache[provider_type]
def __getitem__(self, key: str) -> dict:
return self._load_provider(key)
def __iter__(self):
return iter(self._provider_types)
def __len__(self) -> int:
return len(self._provider_types)
def __contains__(self, key: object) -> bool:
return key in self._provider_types_set
def get(self, key: str, default=None):
if key not in self._provider_types_set:
return default
return self._load_provider(key)
def __repr__(self) -> str: # pragma: no cover - debugging helper
loaded = ", ".join(sorted(self._cache))
return f"{self.__class__.__name__}(loaded=[{loaded}])"
class LazyChecksMapping(Mapping):
"""Lazy-load checks mapping per provider on first access."""
def __init__(self, provider_types: Iterable[str] | None = None) -> None:
if provider_types is None:
provider_types = Provider.ProviderChoices.values
self._provider_types = tuple(provider_types)
self._provider_types_set = set(self._provider_types)
self._cache: dict[str, dict] = {}
def _load_provider(self, provider_type: str) -> dict:
if provider_type not in self._provider_types_set:
raise KeyError(provider_type)
cached = self._cache.get(provider_type)
if cached is not None:
return cached
_ensure_provider_loaded(provider_type)
return self._cache[provider_type]
def __getitem__(self, key: str) -> dict:
return self._load_provider(key)
def __iter__(self):
return iter(self._provider_types)
def __len__(self) -> int:
return len(self._provider_types)
def __contains__(self, key: object) -> bool:
return key in self._provider_types_set
def get(self, key: str, default=None):
if key not in self._provider_types_set:
return default
return self._load_provider(key)
def __repr__(self) -> str: # pragma: no cover - debugging helper
loaded = ", ".join(sorted(self._cache))
return f"{self.__class__.__name__}(loaded=[{loaded}])"
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE = LazyComplianceTemplate()
PROWLER_CHECKS = LazyChecksMapping()
def get_compliance_frameworks(provider_type: Provider.ProviderChoices) -> list[str]:
"""
Retrieve and cache the list of available compliance frameworks for a specific cloud provider.
@@ -70,28 +154,35 @@ def get_prowler_provider_compliance(provider_type: Provider.ProviderChoices) ->
return Compliance.get_bulk(provider_type)
def load_prowler_compliance():
"""
Load and initialize the Prowler compliance data and checks for all provider types.
This function retrieves compliance data for all supported provider types,
generates a compliance overview template, and populates the global variables
`PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE` and `PROWLER_CHECKS` with read-only mappings
of the compliance templates and checks, respectively.
"""
global PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE
global PROWLER_CHECKS
prowler_compliance = {
provider_type: get_prowler_provider_compliance(provider_type)
for provider_type in Provider.ProviderChoices.values
}
template = generate_compliance_overview_template(prowler_compliance)
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE = MappingProxyType(template)
PROWLER_CHECKS = MappingProxyType(load_prowler_checks(prowler_compliance))
def _load_provider_assets(provider_type: Provider.ProviderChoices) -> tuple[dict, dict]:
prowler_compliance = {provider_type: get_prowler_provider_compliance(provider_type)}
template = generate_compliance_overview_template(
prowler_compliance, provider_types=[provider_type]
)
checks = load_prowler_checks(prowler_compliance, provider_types=[provider_type])
return template.get(provider_type, {}), checks.get(provider_type, {})
def load_prowler_checks(prowler_compliance):
def _ensure_provider_loaded(provider_type: Provider.ProviderChoices) -> None:
if (
provider_type in PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE._cache
and provider_type in PROWLER_CHECKS._cache
):
return
template_cached = PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE._cache.get(provider_type)
checks_cached = PROWLER_CHECKS._cache.get(provider_type)
if template_cached is not None and checks_cached is not None:
return
template, checks = _load_provider_assets(provider_type)
if template_cached is None:
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE._cache[provider_type] = template
if checks_cached is None:
PROWLER_CHECKS._cache[provider_type] = checks
def load_prowler_checks(
prowler_compliance, provider_types: Iterable[str] | None = None
):
"""
Generate a mapping of checks to the compliance frameworks that include them.
@@ -100,21 +191,25 @@ def load_prowler_checks(prowler_compliance):
of compliance names that include that check.
Args:
prowler_compliance (dict): The compliance data for all provider types,
prowler_compliance (dict): The compliance data for provider types,
as returned by `get_prowler_provider_compliance`.
provider_types (Iterable[str] | None): Optional subset of provider types to
process. Defaults to all providers.
Returns:
dict: A nested dictionary where the first-level keys are provider types,
and the values are dictionaries mapping check IDs to sets of compliance names.
"""
checks = {}
for provider_type in Provider.ProviderChoices.values:
if provider_types is None:
provider_types = Provider.ProviderChoices.values
for provider_type in provider_types:
checks[provider_type] = {
check_id: set() for check_id in get_prowler_provider_checks(provider_type)
}
for compliance_name, compliance_data in prowler_compliance[
provider_type
].items():
for compliance_name, compliance_data in prowler_compliance.get(
provider_type, {}
).items():
for requirement in compliance_data.Requirements:
for check in requirement.Checks:
try:
@@ -163,7 +258,9 @@ def generate_scan_compliance(
] += 1
def generate_compliance_overview_template(prowler_compliance: dict):
def generate_compliance_overview_template(
prowler_compliance: dict, provider_types: Iterable[str] | None = None
):
"""
Generate a compliance overview template for all provider types.
@@ -173,17 +270,21 @@ def generate_compliance_overview_template(prowler_compliance: dict):
counts for requirements status.
Args:
prowler_compliance (dict): The compliance data for all provider types,
prowler_compliance (dict): The compliance data for provider types,
as returned by `get_prowler_provider_compliance`.
provider_types (Iterable[str] | None): Optional subset of provider types to
process. Defaults to all providers.
Returns:
dict: A nested dictionary representing the compliance overview template,
structured by provider type and compliance framework.
"""
template = {}
for provider_type in Provider.ProviderChoices.values:
if provider_types is None:
provider_types = Provider.ProviderChoices.values
for provider_type in provider_types:
provider_compliance = template.setdefault(provider_type, {})
compliance_data_dict = prowler_compliance[provider_type]
compliance_data_dict = prowler_compliance.get(provider_type, {})
for compliance_name, compliance_data in compliance_data_dict.items():
compliance_requirements = {}
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.18.0
version: 1.19.0
description: |-
Prowler API specification.
+83 -1
View File
@@ -1,10 +1,13 @@
import os
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from django.conf import settings
import api
import api.apps as api_apps_module
from api.apps import (
ApiConfig,
@@ -150,3 +153,82 @@ def test_ensure_crypto_keys_skips_when_env_vars(monkeypatch, tmp_path):
# Assert: orchestrator did not trigger generation when env present
assert called["ensure"] is False
@pytest.fixture(autouse=True)
def stub_api_modules():
"""Provide dummy modules imported during ApiConfig.ready()."""
created = []
for name in ("api.schema_extensions", "api.signals"):
if name not in sys.modules:
sys.modules[name] = types.ModuleType(name)
created.append(name)
yield
for name in created:
sys.modules.pop(name, None)
def _set_argv(monkeypatch, argv):
monkeypatch.setattr(sys, "argv", argv, raising=False)
def _set_testing(monkeypatch, value):
monkeypatch.setattr(settings, "TESTING", value, raising=False)
def _make_app():
return ApiConfig("api", api)
def test_ready_initializes_driver_for_api_process(monkeypatch):
config = _make_app()
_set_argv(monkeypatch, ["gunicorn"])
_set_testing(monkeypatch, False)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
config.ready()
init_driver.assert_called_once()
def test_ready_skips_driver_for_celery(monkeypatch):
config = _make_app()
_set_argv(monkeypatch, ["celery", "-A", "api"])
_set_testing(monkeypatch, False)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
config.ready()
init_driver.assert_not_called()
def test_ready_skips_driver_for_manage_py_skip_command(monkeypatch):
config = _make_app()
_set_argv(monkeypatch, ["manage.py", "migrate"])
_set_testing(monkeypatch, False)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
config.ready()
init_driver.assert_not_called()
def test_ready_skips_driver_when_testing(monkeypatch):
config = _make_app()
_set_argv(monkeypatch, ["gunicorn"])
_set_testing(monkeypatch, True)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
config.ready()
init_driver.assert_not_called()
@@ -0,0 +1,303 @@
"""
Tests for Neo4j database lazy initialization.
The Neo4j driver connects on first use by default. API processes may
eagerly initialize the driver during app startup, while Celery workers
remain lazy. These tests validate the database module behavior itself.
"""
import threading
from unittest.mock import MagicMock, patch
import pytest
class TestLazyInitialization:
"""Test that Neo4j driver is initialized lazily on first use."""
@pytest.fixture(autouse=True)
def reset_module_state(self):
"""Reset module-level singleton state before each test."""
import api.attack_paths.database as db_module
original_driver = db_module._driver
db_module._driver = None
yield
db_module._driver = original_driver
def test_driver_not_initialized_at_import(self):
"""Driver should be None after module import (no eager connection)."""
import api.attack_paths.database as db_module
assert db_module._driver is None
@patch("api.attack_paths.database.settings")
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
def test_init_driver_creates_connection_on_first_call(
self, mock_driver_factory, mock_settings
):
"""init_driver() should create connection only when called."""
import api.attack_paths.database as db_module
mock_driver = MagicMock()
mock_driver_factory.return_value = mock_driver
mock_settings.DATABASES = {
"neo4j": {
"HOST": "localhost",
"PORT": 7687,
"USER": "neo4j",
"PASSWORD": "password",
}
}
assert db_module._driver is None
result = db_module.init_driver()
mock_driver_factory.assert_called_once()
mock_driver.verify_connectivity.assert_called_once()
assert result is mock_driver
assert db_module._driver is mock_driver
@patch("api.attack_paths.database.settings")
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
def test_init_driver_returns_cached_driver_on_subsequent_calls(
self, mock_driver_factory, mock_settings
):
"""Subsequent calls should return cached driver without reconnecting."""
import api.attack_paths.database as db_module
mock_driver = MagicMock()
mock_driver_factory.return_value = mock_driver
mock_settings.DATABASES = {
"neo4j": {
"HOST": "localhost",
"PORT": 7687,
"USER": "neo4j",
"PASSWORD": "password",
}
}
first_result = db_module.init_driver()
second_result = db_module.init_driver()
third_result = db_module.init_driver()
# Only one connection attempt
assert mock_driver_factory.call_count == 1
assert mock_driver.verify_connectivity.call_count == 1
# All calls return same instance
assert first_result is second_result is third_result
@patch("api.attack_paths.database.settings")
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
def test_get_driver_delegates_to_init_driver(
self, mock_driver_factory, mock_settings
):
"""get_driver() should use init_driver() for lazy initialization."""
import api.attack_paths.database as db_module
mock_driver = MagicMock()
mock_driver_factory.return_value = mock_driver
mock_settings.DATABASES = {
"neo4j": {
"HOST": "localhost",
"PORT": 7687,
"USER": "neo4j",
"PASSWORD": "password",
}
}
result = db_module.get_driver()
assert result is mock_driver
mock_driver_factory.assert_called_once()
class TestAtexitRegistration:
"""Test that atexit cleanup handler is registered correctly."""
@pytest.fixture(autouse=True)
def reset_module_state(self):
"""Reset module-level singleton state before each test."""
import api.attack_paths.database as db_module
original_driver = db_module._driver
db_module._driver = None
yield
db_module._driver = original_driver
@patch("api.attack_paths.database.settings")
@patch("api.attack_paths.database.atexit.register")
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
def test_atexit_registered_on_first_init(
self, mock_driver_factory, mock_atexit_register, mock_settings
):
"""atexit.register should be called on first initialization."""
import api.attack_paths.database as db_module
mock_driver_factory.return_value = MagicMock()
mock_settings.DATABASES = {
"neo4j": {
"HOST": "localhost",
"PORT": 7687,
"USER": "neo4j",
"PASSWORD": "password",
}
}
db_module.init_driver()
mock_atexit_register.assert_called_once_with(db_module.close_driver)
@patch("api.attack_paths.database.settings")
@patch("api.attack_paths.database.atexit.register")
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
def test_atexit_registered_only_once(
self, mock_driver_factory, mock_atexit_register, mock_settings
):
"""atexit.register should only be called once across multiple inits.
The double-checked locking on _driver ensures the atexit registration
block only executes once (when _driver is first created).
"""
import api.attack_paths.database as db_module
mock_driver_factory.return_value = MagicMock()
mock_settings.DATABASES = {
"neo4j": {
"HOST": "localhost",
"PORT": 7687,
"USER": "neo4j",
"PASSWORD": "password",
}
}
db_module.init_driver()
db_module.init_driver()
db_module.init_driver()
# Only registered once because subsequent calls hit the fast path
assert mock_atexit_register.call_count == 1
class TestCloseDriver:
"""Test driver cleanup functionality."""
@pytest.fixture(autouse=True)
def reset_module_state(self):
"""Reset module-level singleton state before each test."""
import api.attack_paths.database as db_module
original_driver = db_module._driver
db_module._driver = None
yield
db_module._driver = original_driver
def test_close_driver_closes_and_clears_driver(self):
"""close_driver() should close the driver and set it to None."""
import api.attack_paths.database as db_module
mock_driver = MagicMock()
db_module._driver = mock_driver
db_module.close_driver()
mock_driver.close.assert_called_once()
assert db_module._driver is None
def test_close_driver_handles_none_driver(self):
"""close_driver() should handle case where driver is None."""
import api.attack_paths.database as db_module
db_module._driver = None
# Should not raise
db_module.close_driver()
assert db_module._driver is None
def test_close_driver_clears_driver_even_on_close_error(self):
"""Driver should be cleared even if close() raises an exception."""
import api.attack_paths.database as db_module
mock_driver = MagicMock()
mock_driver.close.side_effect = Exception("Connection error")
db_module._driver = mock_driver
with pytest.raises(Exception, match="Connection error"):
db_module.close_driver()
# Driver should still be cleared
assert db_module._driver is None
class TestThreadSafety:
"""Test thread-safe initialization."""
@pytest.fixture(autouse=True)
def reset_module_state(self):
"""Reset module-level singleton state before each test."""
import api.attack_paths.database as db_module
original_driver = db_module._driver
db_module._driver = None
yield
db_module._driver = original_driver
@patch("api.attack_paths.database.settings")
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
def test_concurrent_init_creates_single_driver(
self, mock_driver_factory, mock_settings
):
"""Multiple threads calling init_driver() should create only one driver."""
import api.attack_paths.database as db_module
mock_driver = MagicMock()
mock_driver_factory.return_value = mock_driver
mock_settings.DATABASES = {
"neo4j": {
"HOST": "localhost",
"PORT": 7687,
"USER": "neo4j",
"PASSWORD": "password",
}
}
results = []
errors = []
def call_init():
try:
result = db_module.init_driver()
results.append(result)
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=call_init) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, f"Threads raised errors: {errors}"
# Only one driver created
assert mock_driver_factory.call_count == 1
# All threads got the same driver instance
assert all(r is mock_driver for r in results)
assert len(results) == 10
@@ -6,7 +6,6 @@ from api.compliance import (
get_prowler_provider_checks,
get_prowler_provider_compliance,
load_prowler_checks,
load_prowler_compliance,
)
from api.models import Provider
@@ -35,55 +34,6 @@ class TestCompliance:
assert compliance_data == mock_compliance.get_bulk.return_value
mock_compliance.get_bulk.assert_called_once_with(provider_type)
@patch("api.models.Provider.ProviderChoices")
@patch("api.compliance.get_prowler_provider_compliance")
@patch("api.compliance.generate_compliance_overview_template")
@patch("api.compliance.load_prowler_checks")
def test_load_prowler_compliance(
self,
mock_load_prowler_checks,
mock_generate_compliance_overview_template,
mock_get_prowler_provider_compliance,
mock_provider_choices,
):
mock_provider_choices.values = ["aws", "azure"]
compliance_data_aws = {"compliance_aws": MagicMock()}
compliance_data_azure = {"compliance_azure": MagicMock()}
compliance_data_dict = {
"aws": compliance_data_aws,
"azure": compliance_data_azure,
}
def mock_get_compliance(provider_type):
return compliance_data_dict[provider_type]
mock_get_prowler_provider_compliance.side_effect = mock_get_compliance
mock_generate_compliance_overview_template.return_value = {
"template_key": "template_value"
}
mock_load_prowler_checks.return_value = {"checks_key": "checks_value"}
load_prowler_compliance()
from api.compliance import PROWLER_CHECKS, PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE
assert PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE == {
"template_key": "template_value"
}
assert PROWLER_CHECKS == {"checks_key": "checks_value"}
expected_prowler_compliance = compliance_data_dict
mock_get_prowler_provider_compliance.assert_any_call("aws")
mock_get_prowler_provider_compliance.assert_any_call("azure")
mock_generate_compliance_overview_template.assert_called_once_with(
expected_prowler_compliance
)
mock_load_prowler_checks.assert_called_once_with(expected_prowler_compliance)
@patch("api.compliance.get_prowler_provider_checks")
@patch("api.models.Provider.ProviderChoices")
def test_load_prowler_checks(
+3
View File
@@ -3867,6 +3867,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
return_value=graph_payload,
) as mock_execute,
patch("api.v1.views.graph_database.clear_cache") as mock_clear_cache,
):
response = authenticated_client.post(
reverse(
@@ -3889,6 +3890,7 @@ class TestAttackPathsScanViewSet:
query_definition,
prepared_parameters,
)
mock_clear_cache.assert_called_once_with(attack_paths_scan.graph_database)
result = response.json()["data"]
attributes = result["attributes"]
assert attributes["nodes"] == graph_payload["nodes"]
@@ -4000,6 +4002,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
return_value={"nodes": [], "relationships": []},
),
patch("api.v1.views.graph_database.clear_cache"),
):
response = authenticated_client.post(
reverse(
+47 -10
View File
@@ -1,4 +1,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import TYPE_CHECKING
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from django.contrib.postgres.aggregates import ArrayAgg
@@ -11,19 +14,25 @@ from api.exceptions import InvitationTokenExpiredException
from api.models import Integration, Invitation, Processor, Provider, Resource
from api.v1.serializers import FindingMetadataSerializer
from prowler.lib.outputs.jira.jira import Jira, JiraBasicAuthError
from prowler.providers.alibabacloud.alibabacloud_provider import AlibabacloudProvider
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.s3.s3 import S3
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.common.models import Connection
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.iac.iac_provider import IacProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
if TYPE_CHECKING:
from prowler.providers.alibabacloud.alibabacloud_provider import (
AlibabacloudProvider,
)
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.iac.iac_provider import IacProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.mongodbatlas.mongodbatlas_provider import (
MongodbatlasProvider,
)
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
class CustomOAuth2Client(OAuth2Client):
@@ -89,24 +98,52 @@ def return_prowler_provider(
"""
match provider.provider:
case Provider.ProviderChoices.AWS.value:
from prowler.providers.aws.aws_provider import AwsProvider
prowler_provider = AwsProvider
case Provider.ProviderChoices.GCP.value:
from prowler.providers.gcp.gcp_provider import GcpProvider
prowler_provider = GcpProvider
case Provider.ProviderChoices.AZURE.value:
from prowler.providers.azure.azure_provider import AzureProvider
prowler_provider = AzureProvider
case Provider.ProviderChoices.KUBERNETES.value:
from prowler.providers.kubernetes.kubernetes_provider import (
KubernetesProvider,
)
prowler_provider = KubernetesProvider
case Provider.ProviderChoices.M365.value:
from prowler.providers.m365.m365_provider import M365Provider
prowler_provider = M365Provider
case Provider.ProviderChoices.GITHUB.value:
from prowler.providers.github.github_provider import GithubProvider
prowler_provider = GithubProvider
case Provider.ProviderChoices.MONGODBATLAS.value:
from prowler.providers.mongodbatlas.mongodbatlas_provider import (
MongodbatlasProvider,
)
prowler_provider = MongodbatlasProvider
case Provider.ProviderChoices.IAC.value:
from prowler.providers.iac.iac_provider import IacProvider
prowler_provider = IacProvider
case Provider.ProviderChoices.ORACLECLOUD.value:
from prowler.providers.oraclecloud.oraclecloud_provider import (
OraclecloudProvider,
)
prowler_provider = OraclecloudProvider
case Provider.ProviderChoices.ALIBABACLOUD.value:
from prowler.providers.alibabacloud.alibabacloud_provider import (
AlibabacloudProvider,
)
prowler_provider = AlibabacloudProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
+4 -2
View File
@@ -92,6 +92,7 @@ from tasks.tasks import (
refresh_lighthouse_provider_models_task,
)
from api.attack_paths import database as graph_database
from api.attack_paths import get_queries_for_provider, get_query_by_id
from api.attack_paths import views_helpers as attack_paths_views_helpers
from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
@@ -393,7 +394,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.18.0"
spectacular_settings.VERSION = "1.19.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -2652,6 +2653,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
graph = attack_paths_views_helpers.execute_attack_paths_query(
attack_paths_scan, query_definition, parameters
)
graph_database.clear_cache(attack_paths_scan.graph_database)
status_code = status.HTTP_200_OK
if not graph.get("nodes"):
@@ -4665,7 +4667,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
# If we couldn't determine from database, try each provider type
if not provider_type:
for pt in Provider.ProviderChoices.values:
if compliance_id in PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE.get(pt, {}):
if compliance_id in get_compliance_frameworks(pt):
provider_type = pt
break
@@ -59,6 +59,7 @@ def start_aws_ingestion(
)
# Starting with sync functions
logger.info(f"Syncing organizations for AWS account {prowler_api_provider.uid}")
cartography_aws.organizations.sync(
neo4j_session,
{prowler_api_provider.alias: prowler_api_provider.uid},
@@ -84,13 +85,22 @@ def start_aws_ingestion(
)
if "permission_relationships" in requested_syncs:
logger.info(
f"Syncing function permission_relationships for AWS account {prowler_api_provider.uid}"
)
cartography_aws.RESOURCE_FUNCTIONS["permission_relationships"](**sync_args)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 88)
if "resourcegroupstaggingapi" in requested_syncs:
logger.info(
f"Syncing function resourcegroupstaggingapi for AWS account {prowler_api_provider.uid}"
)
cartography_aws.RESOURCE_FUNCTIONS["resourcegroupstaggingapi"](**sync_args)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 89)
logger.info(
f"Syncing ec2_iaminstanceprofile scoped analysis for AWS account {prowler_api_provider.uid}"
)
cartography_aws.run_scoped_analysis_job(
"aws_ec2_iaminstanceprofile.json",
neo4j_session,
@@ -98,6 +108,9 @@ def start_aws_ingestion(
)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 90)
logger.info(
f"Syncing lambda_ecr analysis for AWS account {prowler_api_provider.uid}"
)
cartography_aws.run_analysis_job(
"aws_lambda_ecr.json",
neo4j_session,
@@ -105,6 +118,7 @@ def start_aws_ingestion(
)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 91)
logger.info(f"Syncing metadata for AWS account {prowler_api_provider.uid}")
cartography_aws.merge_module_sync_metadata(
neo4j_session,
group_type="AWSAccount",
@@ -118,6 +132,7 @@ def start_aws_ingestion(
# Removing the added extra field
del common_job_parameters["AWS_ID"]
logger.info(f"Syncing cleanup_job for AWS account {prowler_api_provider.uid}")
cartography_aws.run_cleanup_job(
"aws_post_ingestion_principals_cleanup.json",
neo4j_session,
@@ -125,6 +140,7 @@ def start_aws_ingestion(
)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 93)
logger.info(f"Syncing analysis for AWS account {prowler_api_provider.uid}")
cartography_aws._perform_aws_analysis(
requested_syncs, neo4j_session, common_job_parameters
)
@@ -1,6 +1,7 @@
from datetime import datetime, timezone
from typing import Any
from django.db.models import Q
from cartography.config import Config as CartographyConfig
from api.db_utils import rls_transaction
@@ -153,9 +154,15 @@ def get_provider_graph_database_names(tenant_id: str, provider_id: str) -> list[
Note: For accesing the `AttackPathsScan` we need to use `all_objects` manager because the provider is soft-deleted.
"""
with rls_transaction(tenant_id):
graph_databases_names_qs = ProwlerAPIAttackPathsScan.all_objects.filter(
provider_id=provider_id,
is_graph_database_deleted=False,
).values_list("graph_database", flat=True)
graph_databases_names_qs = (
ProwlerAPIAttackPathsScan.all_objects.filter(
~Q(graph_database=""),
graph_database__isnull=False,
provider_id=provider_id,
is_graph_database_deleted=False,
)
.values_list("graph_database", flat=True)
.distinct()
)
return list(graph_databases_names_qs)
@@ -1,18 +1,21 @@
import neo4j
from collections import defaultdict
from typing import Generator
import neo4j
from cartography.client.core.tx import run_write_query
from cartography.config import Config as CartographyConfig
from celery.utils.log import get_task_logger
from api.db_utils import rls_transaction
from api.models import Provider, ResourceFindingMapping
from config.env import env
from prowler.config import config as ProwlerConfig
from tasks.jobs.attack_paths.providers import get_node_uid_field, get_root_node_label
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding, Provider, ResourceFindingMapping
from prowler.config import config as ProwlerConfig
logger = get_task_logger(__name__)
BATCH_SIZE = env.int("NEO4J_INSERT_BATCH_SIZE", 500)
BATCH_SIZE = env.int("ATTACK_PATHS_FINDINGS_BATCH_SIZE", 1000)
INDEX_STATEMENTS = [
"CREATE INDEX prowler_finding_id IF NOT EXISTS FOR (n:ProwlerFinding) ON (n.id);",
@@ -22,12 +25,18 @@ INDEX_STATEMENTS = [
]
INSERT_STATEMENT_TEMPLATE = """
MATCH (account:__ROOT_NODE_LABEL__ {id: $provider_uid})
UNWIND $findings_data AS finding_data
MATCH (account:__ROOT_NODE_LABEL__ {id: $provider_uid})
MATCH (account)-->(resource)
WHERE resource.__NODE_UID_FIELD__ = finding_data.resource_uid
OR resource.id = finding_data.resource_uid
OPTIONAL MATCH (account)-->(resource_by_uid)
WHERE resource_by_uid.__NODE_UID_FIELD__ = finding_data.resource_uid
WITH account, finding_data, resource_by_uid
OPTIONAL MATCH (account)-->(resource_by_id)
WHERE resource_by_uid IS NULL
AND resource_by_id.id = finding_data.resource_uid
WITH account, finding_data, COALESCE(resource_by_uid, resource_by_id) AS resource
WHERE resource IS NOT NULL
MERGE (finding:ProwlerFinding {id: finding_data.id})
ON CREATE SET
@@ -83,9 +92,8 @@ def create_indexes(neo4j_session: neo4j.Session) -> None:
Code based on Cartography version 0.122.0, specifically on `cartography.intel.create_indexes.run`.
"""
logger.info("Creating indexes for Prowler node types.")
logger.info("Creating indexes for Prowler Findings node types")
for statement in INDEX_STATEMENTS:
logger.debug("Executing statement: %s", statement)
run_write_query(neo4j_session, statement)
@@ -103,58 +111,131 @@ def analysis(
def get_provider_last_scan_findings(
prowler_api_provider: Provider,
scan_id: str,
) -> list[dict[str, str]]:
with rls_transaction(prowler_api_provider.tenant_id):
resource_finding_qs = ResourceFindingMapping.objects.filter(
finding__scan_id=scan_id,
).values(
"resource__uid",
"finding__id",
"finding__uid",
"finding__inserted_at",
"finding__updated_at",
"finding__first_seen_at",
"finding__scan_id",
"finding__delta",
"finding__status",
"finding__status_extended",
"finding__severity",
"finding__check_id",
"finding__check_metadata__checktitle",
"finding__muted",
"finding__muted_reason",
)
) -> Generator[list[dict[str, str]], None, None]:
"""
Generator that yields batches of finding-resource pairs.
findings = []
for resource_finding in resource_finding_qs:
findings.append(
Two-step query approach per batch:
1. Paginate findings for scan (single table, indexed by scan_id)
2. Batch-fetch resource UIDs via mapping table (single join)
3. Merge and yield flat structure for Neo4j
Memory efficient: never holds more than BATCH_SIZE findings in memory.
"""
logger.info(
f"Starting findings fetch for scan {scan_id} (tenant {prowler_api_provider.tenant_id}) with batch size {BATCH_SIZE}"
)
iteration = 0
last_id = None
while True:
iteration += 1
with rls_transaction(prowler_api_provider.tenant_id, using=READ_REPLICA_ALIAS):
# Use all_objects to avoid the ActiveProviderManager's implicit JOIN
# through Scan -> Provider (to check is_deleted=False).
# The provider is already validated as active in this context.
qs = Finding.all_objects.filter(scan_id=scan_id).order_by("id")
if last_id is not None:
qs = qs.filter(id__gt=last_id)
findings_batch = list(
qs.values(
"id",
"uid",
"inserted_at",
"updated_at",
"first_seen_at",
"scan_id",
"delta",
"status",
"status_extended",
"severity",
"check_id",
"check_metadata__checktitle",
"muted",
"muted_reason",
)[:BATCH_SIZE]
)
logger.info(
f"Iteration #{iteration} fetched {len(findings_batch)} findings"
)
if not findings_batch:
logger.info(
f"No findings returned for iteration #{iteration}; stopping pagination"
)
break
last_id = findings_batch[-1]["id"]
enriched_batch = _enrich_and_flatten_batch(findings_batch)
# Yield outside the transaction
if enriched_batch:
yield enriched_batch
logger.info(f"Finished fetching findings for scan {scan_id}")
def _enrich_and_flatten_batch(
findings_batch: list[dict],
) -> list[dict[str, str]]:
"""
Fetch resource UIDs for a batch of findings and return flat structure.
One finding with 3 resources becomes 3 dicts (same output format as before).
Must be called within an RLS transaction context.
"""
finding_ids = [f["id"] for f in findings_batch]
# Single join: mapping -> resource
resource_mappings = ResourceFindingMapping.objects.filter(
finding_id__in=finding_ids
).values_list("finding_id", "resource__uid")
# Build finding_id -> [resource_uids] mapping
finding_resources = defaultdict(list)
for finding_id, resource_uid in resource_mappings:
finding_resources[finding_id].append(resource_uid)
# Flatten: one dict per (finding, resource) pair
results = []
for f in findings_batch:
resource_uids = finding_resources.get(f["id"], [])
if not resource_uids:
continue
for resource_uid in resource_uids:
results.append(
{
"resource_uid": str(resource_finding["resource__uid"]),
"id": str(resource_finding["finding__id"]),
"uid": resource_finding["finding__uid"],
"inserted_at": resource_finding["finding__inserted_at"],
"updated_at": resource_finding["finding__updated_at"],
"first_seen_at": resource_finding["finding__first_seen_at"],
"scan_id": str(resource_finding["finding__scan_id"]),
"delta": resource_finding["finding__delta"],
"status": resource_finding["finding__status"],
"status_extended": resource_finding["finding__status_extended"],
"severity": resource_finding["finding__severity"],
"check_id": str(resource_finding["finding__check_id"]),
"check_title": resource_finding[
"finding__check_metadata__checktitle"
],
"muted": resource_finding["finding__muted"],
"muted_reason": resource_finding["finding__muted_reason"],
"resource_uid": str(resource_uid),
"id": str(f["id"]),
"uid": f["uid"],
"inserted_at": f["inserted_at"],
"updated_at": f["updated_at"],
"first_seen_at": f["first_seen_at"],
"scan_id": str(f["scan_id"]),
"delta": f["delta"],
"status": f["status"],
"status_extended": f["status_extended"],
"severity": f["severity"],
"check_id": str(f["check_id"]),
"check_title": f["check_metadata__checktitle"],
"muted": f["muted"],
"muted_reason": f["muted_reason"],
}
)
return findings
return results
def load_findings(
neo4j_session: neo4j.Session,
findings_data: list[dict[str, str]],
findings_batches: Generator[list[dict[str, str]], None, None],
prowler_api_provider: Provider,
config: CartographyConfig,
) -> None:
@@ -172,16 +253,20 @@ def load_findings(
"prowler_version": ProwlerConfig.prowler_version,
}
total_length = len(findings_data)
for i in range(0, total_length, BATCH_SIZE):
parameters["findings_data"] = findings_data[i : i + BATCH_SIZE]
batch_num = 0
total_records = 0
for batch in findings_batches:
batch_num += 1
batch_size = len(batch)
total_records += batch_size
logger.info(
f"Loading findings batch {i // BATCH_SIZE + 1} / {(total_length + BATCH_SIZE - 1) // BATCH_SIZE}"
)
parameters["findings_data"] = batch
logger.info(f"Loading findings batch {batch_num} ({batch_size} records)")
neo4j_session.run(query, parameters)
logger.info(f"Finished loading {total_records} records in {batch_num} batches")
def cleanup_findings(
neo4j_session: neo4j.Session,
@@ -117,17 +117,31 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
)
# Post-processing: Just keeping it to be more Cartography compliant
logger.info(
f"Syncing Cartography ontology for AWS account {prowler_api_provider.uid}"
)
cartography_ontology.run(neo4j_session, cartography_config)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 95)
logger.info(
f"Syncing Cartography analysis for AWS account {prowler_api_provider.uid}"
)
cartography_analysis.run(neo4j_session, cartography_config)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 96)
# Adding Prowler nodes and relationships
logger.info(
f"Syncing Prowler analysis for AWS account {prowler_api_provider.uid}"
)
prowler.analysis(
neo4j_session, prowler_api_provider, scan_id, cartography_config
)
logger.info(
f"Clearing Neo4j cache for database {cartography_config.neo4j_database}"
)
graph_database.clear_cache(cartography_config.neo4j_database)
logger.info(
f"Completed Cartography ({attack_paths_scan.id}) for "
f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}"
+58 -63
View File
@@ -1,25 +1,13 @@
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from shutil import rmtree
from celery import chain, group, shared_task
from celery.utils.log import get_task_logger
from django_celery_beat.models import PeriodicTask
from api.compliance import get_compliance_frameworks
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.decorators import handle_provider_deletion, set_tenant
from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices
from api.utils import initialize_prowler_provider
from api.v1.serializers import ScanTaskSerializer
from config.celery import RLSTask
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
from prowler.lib.outputs.finding import Finding as FindingOutput
from django_celery_beat.models import PeriodicTask
from tasks.jobs.attack_paths import (
attack_paths_scan,
can_provider_run_attack_paths_scan,
@@ -64,7 +52,22 @@ from tasks.jobs.scan import (
perform_prowler_scan,
update_provider_compliance_scores,
)
from tasks.utils import batched, get_next_execution_datetime
from tasks.utils import (
_get_or_create_scheduled_scan,
batched,
get_next_execution_datetime,
)
from api.compliance import get_compliance_frameworks
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.decorators import handle_provider_deletion, set_tenant
from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices
from api.utils import initialize_prowler_provider
from api.v1.serializers import ScanTaskSerializer
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
from prowler.lib.outputs.finding import Finding as FindingOutput
logger = get_task_logger(__name__)
@@ -275,44 +278,38 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
periodic_task_instance = PeriodicTask.objects.get(
name=f"scan-perform-scheduled-{provider_id}"
)
executed_scan = Scan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
task__task_runner_task__task_id=task_id,
).order_by("completed_at")
if (
executing_scan = (
Scan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.EXECUTING,
scheduler_task_id=periodic_task_instance.id,
scheduled_at__date=datetime.now(timezone.utc).date(),
).exists()
or executed_scan.exists()
):
# Duplicated task execution due to visibility timeout or scan is already running
logger.warning(f"Duplicated scheduled scan for provider {provider_id}.")
try:
affected_scan = executed_scan.first()
if not affected_scan:
raise ValueError(
"Error retrieving affected scan details after detecting duplicated scheduled "
"scan."
)
# Return the affected scan details to avoid losing data
serializer = ScanTaskSerializer(instance=affected_scan)
except Exception as duplicated_scan_exception:
logger.error(
f"Duplicated scheduled scan for provider {provider_id}. Error retrieving affected scan details: "
f"{str(duplicated_scan_exception)}"
)
raise duplicated_scan_exception
return serializer.data
)
.order_by("-started_at")
.first()
)
if executing_scan:
logger.warning(
f"Scheduled scan already executing for provider {provider_id}. Skipping."
)
return ScanTaskSerializer(instance=executing_scan).data
executed_scan = Scan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
task__task_runner_task__task_id=task_id,
).first()
if executed_scan:
# Duplicated task execution due to visibility timeout
logger.warning(f"Duplicated scheduled scan for provider {provider_id}.")
return ScanTaskSerializer(instance=executed_scan).data
interval = periodic_task_instance.interval
next_scan_datetime = get_next_execution_datetime(task_id, provider_id)
current_scan_datetime = next_scan_datetime - timedelta(
**{interval.period: interval.every}
)
# TEMPORARY WORKAROUND: Clean up orphan scans from transaction isolation issue
_cleanup_orphan_scheduled_scans(
@@ -321,19 +318,12 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
scheduler_task_id=periodic_task_instance.id,
)
scan_instance, _ = Scan.objects.get_or_create(
scan_instance = _get_or_create_scheduled_scan(
tenant_id=tenant_id,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
scheduler_task_id=periodic_task_instance.id,
defaults={
"state": StateChoices.SCHEDULED,
"name": "Daily scheduled scan",
"scheduled_at": next_scan_datetime - timedelta(days=1),
},
scheduled_at=current_scan_datetime,
)
scan_instance.task_id = task_id
scan_instance.save()
@@ -343,18 +333,19 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
scan_id=str(scan_instance.id),
provider_id=provider_id,
)
except Exception as e:
raise e
finally:
with rls_transaction(tenant_id):
Scan.objects.get_or_create(
now = datetime.now(timezone.utc)
if next_scan_datetime <= now:
interval_delta = timedelta(**{interval.period: interval.every})
while next_scan_datetime <= now:
next_scan_datetime += interval_delta
_get_or_create_scheduled_scan(
tenant_id=tenant_id,
name="Daily scheduled scan",
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
scheduled_at=next_scan_datetime,
scheduler_task_id=periodic_task_instance.id,
scheduled_at=next_scan_datetime,
update_state=True,
)
_perform_scan_complete_tasks(tenant_id, str(scan_instance.id), provider_id)
@@ -368,8 +359,12 @@ def perform_scan_summary_task(tenant_id: str, scan_id: str):
return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id)
# TODO: This task must be queued at the `attack-paths` queue, don't forget to add it to the `docker-entrypoint.sh` file
@shared_task(base=RLSTask, bind=True, name="attack-paths-scan-perform", queue="scans")
@shared_task(
base=RLSTask,
bind=True,
name="attack-paths-scan-perform",
queue="attack-paths-scans",
)
def perform_attack_paths_scan_task(self, tenant_id: str, scan_id: str):
"""
Execute an Attack Paths scan for the given provider within the current tenant RLS context.
@@ -3,6 +3,8 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, call, patch
import pytest
from tasks.jobs.attack_paths import prowler as prowler_module
from tasks.jobs.attack_paths.scan import run as attack_paths_run
from api.models import (
AttackPathsScan,
@@ -15,8 +17,6 @@ from api.models import (
StatusChoices,
)
from prowler.lib.check.models import Severity
from tasks.jobs.attack_paths import prowler as prowler_module
from tasks.jobs.attack_paths.scan import run as attack_paths_run
@pytest.mark.django_db
@@ -68,6 +68,7 @@ class TestAttackPathsRun:
"tasks.jobs.attack_paths.scan.graph_database.get_session",
return_value=session_ctx,
) as mock_get_session,
patch("tasks.jobs.attack_paths.scan.graph_database.clear_cache"),
patch(
"tasks.jobs.attack_paths.scan.cartography_create_indexes.run"
) as mock_cartography_indexes,
@@ -276,15 +277,15 @@ class TestAttackPathsProwlerHelpers:
provider.provider = Provider.ProviderChoices.AWS
provider.save()
findings = [
{"id": "1", "resource_uid": "r-1"},
{"id": "2", "resource_uid": "r-2"},
]
# Create a generator that yields two batches
def findings_generator():
yield [{"id": "1", "resource_uid": "r-1"}]
yield [{"id": "2", "resource_uid": "r-2"}]
config = SimpleNamespace(update_tag=12345)
mock_session = MagicMock()
with (
patch.object(prowler_module, "BATCH_SIZE", 1),
patch(
"tasks.jobs.attack_paths.prowler.get_root_node_label",
return_value="AWSAccount",
@@ -294,7 +295,9 @@ class TestAttackPathsProwlerHelpers:
return_value="arn",
),
):
prowler_module.load_findings(mock_session, findings, provider, config)
prowler_module.load_findings(
mock_session, findings_generator(), provider, config
)
assert mock_session.run.call_count == 2
for call_args in mock_session.run.call_args_list:
@@ -402,11 +405,18 @@ class TestAttackPathsProwlerHelpers:
with patch(
"tasks.jobs.attack_paths.prowler.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
), patch(
"tasks.jobs.attack_paths.prowler.READ_REPLICA_ALIAS",
"default",
):
findings_data = prowler_module.get_provider_last_scan_findings(
# Generator yields batches, collect all findings from all batches
findings_batches = prowler_module.get_provider_last_scan_findings(
provider,
str(latest_scan.id),
)
findings_data = []
for batch in findings_batches:
findings_data.extend(batch)
assert len(findings_data) == 1
finding_dict = findings_data[0]
@@ -414,3 +424,285 @@ class TestAttackPathsProwlerHelpers:
assert finding_dict["resource_uid"] == resource.uid
assert finding_dict["check_title"] == "Check title"
assert finding_dict["scan_id"] == str(latest_scan.id)
def test_enrich_and_flatten_batch_single_resource(
self,
tenants_fixture,
providers_fixture,
):
"""One finding + one resource = one output dict"""
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
resource = Resource.objects.create(
tenant_id=tenant.id,
provider=provider,
uid="resource-uid-1",
name="Resource 1",
region="us-east-1",
service="ec2",
type="instance",
)
scan = Scan.objects.create(
name="Test Scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant.id,
)
finding = Finding.objects.create(
tenant_id=tenant.id,
uid="finding-uid",
scan=scan,
delta=Finding.DeltaChoices.NEW,
status=StatusChoices.FAIL,
status_extended="failed",
severity=Severity.high,
impact=Severity.high,
impact_extended="",
raw_result={},
check_id="check-1",
check_metadata={"checktitle": "Check title"},
first_seen_at=scan.inserted_at,
)
ResourceFindingMapping.objects.create(
tenant_id=tenant.id,
resource=resource,
finding=finding,
)
# Simulate the dict returned by .values()
finding_dict = {
"id": finding.id,
"uid": finding.uid,
"inserted_at": finding.inserted_at,
"updated_at": finding.updated_at,
"first_seen_at": finding.first_seen_at,
"scan_id": scan.id,
"delta": finding.delta,
"status": finding.status,
"status_extended": finding.status_extended,
"severity": finding.severity,
"check_id": finding.check_id,
"check_metadata__checktitle": finding.check_metadata["checktitle"],
"muted": finding.muted,
"muted_reason": finding.muted_reason,
}
# _enrich_and_flatten_batch queries ResourceFindingMapping directly
# No RLS mock needed - test DB doesn't enforce RLS policies
with patch(
"tasks.jobs.attack_paths.prowler.READ_REPLICA_ALIAS",
"default",
):
result = prowler_module._enrich_and_flatten_batch([finding_dict])
assert len(result) == 1
assert result[0]["resource_uid"] == resource.uid
assert result[0]["id"] == str(finding.id)
assert result[0]["status"] == "FAIL"
def test_enrich_and_flatten_batch_multiple_resources(
self,
tenants_fixture,
providers_fixture,
):
"""One finding + three resources = three output dicts"""
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
resources = []
for i in range(3):
resource = Resource.objects.create(
tenant_id=tenant.id,
provider=provider,
uid=f"resource-uid-{i}",
name=f"Resource {i}",
region="us-east-1",
service="ec2",
type="instance",
)
resources.append(resource)
scan = Scan.objects.create(
name="Test Scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant.id,
)
finding = Finding.objects.create(
tenant_id=tenant.id,
uid="finding-uid",
scan=scan,
delta=Finding.DeltaChoices.NEW,
status=StatusChoices.FAIL,
status_extended="failed",
severity=Severity.high,
impact=Severity.high,
impact_extended="",
raw_result={},
check_id="check-1",
check_metadata={"checktitle": "Check title"},
first_seen_at=scan.inserted_at,
)
# Map finding to all 3 resources
for resource in resources:
ResourceFindingMapping.objects.create(
tenant_id=tenant.id,
resource=resource,
finding=finding,
)
finding_dict = {
"id": finding.id,
"uid": finding.uid,
"inserted_at": finding.inserted_at,
"updated_at": finding.updated_at,
"first_seen_at": finding.first_seen_at,
"scan_id": scan.id,
"delta": finding.delta,
"status": finding.status,
"status_extended": finding.status_extended,
"severity": finding.severity,
"check_id": finding.check_id,
"check_metadata__checktitle": finding.check_metadata["checktitle"],
"muted": finding.muted,
"muted_reason": finding.muted_reason,
}
# _enrich_and_flatten_batch queries ResourceFindingMapping directly
# No RLS mock needed - test DB doesn't enforce RLS policies
with patch(
"tasks.jobs.attack_paths.prowler.READ_REPLICA_ALIAS",
"default",
):
result = prowler_module._enrich_and_flatten_batch([finding_dict])
assert len(result) == 3
result_resource_uids = {r["resource_uid"] for r in result}
assert result_resource_uids == {r.uid for r in resources}
# All should have same finding data
for r in result:
assert r["id"] == str(finding.id)
assert r["status"] == "FAIL"
def test_enrich_and_flatten_batch_no_resources_skips(
self,
tenants_fixture,
providers_fixture,
):
"""Finding without resources should be skipped"""
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = Scan.objects.create(
name="Test Scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant.id,
)
finding = Finding.objects.create(
tenant_id=tenant.id,
uid="orphan-finding",
scan=scan,
delta=Finding.DeltaChoices.NEW,
status=StatusChoices.FAIL,
status_extended="failed",
severity=Severity.high,
impact=Severity.high,
impact_extended="",
raw_result={},
check_id="check-1",
check_metadata={"checktitle": "Check title"},
first_seen_at=scan.inserted_at,
)
# Note: No ResourceFindingMapping created
finding_dict = {
"id": finding.id,
"uid": finding.uid,
"inserted_at": finding.inserted_at,
"updated_at": finding.updated_at,
"first_seen_at": finding.first_seen_at,
"scan_id": scan.id,
"delta": finding.delta,
"status": finding.status,
"status_extended": finding.status_extended,
"severity": finding.severity,
"check_id": finding.check_id,
"check_metadata__checktitle": finding.check_metadata["checktitle"],
"muted": finding.muted,
"muted_reason": finding.muted_reason,
}
# Mock logger to verify no warning is emitted
with (
patch(
"tasks.jobs.attack_paths.prowler.READ_REPLICA_ALIAS",
"default",
),
patch("tasks.jobs.attack_paths.prowler.logger") as mock_logger,
):
result = prowler_module._enrich_and_flatten_batch([finding_dict])
assert len(result) == 0
mock_logger.warning.assert_not_called()
def test_generator_is_lazy(self, providers_fixture):
"""Generator should not execute queries until iterated"""
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan_id = "some-scan-id"
with (
patch("tasks.jobs.attack_paths.prowler.rls_transaction") as mock_rls,
patch("tasks.jobs.attack_paths.prowler.Finding") as mock_finding,
):
# Create generator but don't iterate
prowler_module.get_provider_last_scan_findings(provider, scan_id)
# Nothing should be called yet
mock_rls.assert_not_called()
mock_finding.objects.filter.assert_not_called()
def test_load_findings_empty_generator(self, providers_fixture):
"""Empty generator should not call neo4j"""
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
mock_session = MagicMock()
config = SimpleNamespace(update_tag=12345)
def empty_gen():
return
yield # Make it a generator
with (
patch(
"tasks.jobs.attack_paths.prowler.get_root_node_label",
return_value="AWSAccount",
),
patch(
"tasks.jobs.attack_paths.prowler.get_node_uid_field",
return_value="arn",
),
):
prowler_module.load_findings(mock_session, empty_gen(), provider, config)
mock_session.run.assert_not_called()
@@ -417,9 +417,8 @@ class TestProwlerIntegrationConnectionTest:
raise_on_exception=False,
)
@patch("api.utils.AwsProvider")
@patch("api.utils.S3")
def test_s3_integration_connection_failure(self, mock_s3_class, mock_aws_provider):
def test_s3_integration_connection_failure(self, mock_s3_class):
"""Test S3 integration connection failure."""
integration = MagicMock()
integration.integration_type = Integration.IntegrationChoices.AMAZON_S3
@@ -429,9 +428,6 @@ class TestProwlerIntegrationConnectionTest:
}
integration.configuration = {"bucket_name": "test-bucket"}
mock_session = MagicMock()
mock_aws_provider.return_value.session.current_session = mock_session
mock_connection = Connection(
is_connected=False, error=Exception("Bucket not found")
)
+224 -10
View File
@@ -1,21 +1,13 @@
import uuid
from contextlib import contextmanager
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import openai
import pytest
from botocore.exceptions import ClientError
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from api.models import (
Integration,
LighthouseProviderConfiguration,
LighthouseProviderModels,
Scan,
StateChoices,
)
from django_celery_results.models import TaskResult
from tasks.jobs.lighthouse_providers import (
_create_bedrock_client,
_extract_bedrock_credentials,
@@ -27,11 +19,21 @@ from tasks.tasks import (
check_lighthouse_provider_connection_task,
generate_outputs_task,
perform_attack_paths_scan_task,
perform_scheduled_scan_task,
refresh_lighthouse_provider_models_task,
s3_integration_task,
security_hub_integration_task,
)
from api.models import (
Integration,
LighthouseProviderConfiguration,
LighthouseProviderModels,
Scan,
StateChoices,
Task,
)
@pytest.mark.django_db
class TestExtractBedrockCredentials:
@@ -2137,3 +2139,215 @@ class TestCleanupOrphanScheduledScans:
assert not Scan.objects.filter(id=orphan_scan.id).exists()
assert Scan.objects.filter(id=scheduled_scan.id).exists()
assert Scan.objects.filter(id=available_scan_other_task.id).exists()
@pytest.mark.django_db
class TestPerformScheduledScanTask:
"""Unit tests for perform_scheduled_scan_task."""
@staticmethod
@contextmanager
def _override_task_request(task, **attrs):
request = task.request
sentinel = object()
previous = {key: getattr(request, key, sentinel) for key in attrs}
for key, value in attrs.items():
setattr(request, key, value)
try:
yield
finally:
for key, prev in previous.items():
if prev is sentinel:
if hasattr(request, key):
delattr(request, key)
else:
setattr(request, key, prev)
def _create_periodic_task(self, provider_id, tenant_id, interval_hours=24):
interval, _ = IntervalSchedule.objects.get_or_create(
every=interval_hours, period="hours"
)
return PeriodicTask.objects.create(
name=f"scan-perform-scheduled-{provider_id}",
task="scan-perform-scheduled",
interval=interval,
kwargs=f'{{"tenant_id": "{tenant_id}", "provider_id": "{provider_id}"}}',
enabled=True,
)
def _create_task_result(self, tenant_id, task_id):
task_result = TaskResult.objects.create(
task_id=task_id,
task_name="scan-perform-scheduled",
status="STARTED",
date_created=datetime.now(timezone.utc),
)
Task.objects.create(
id=task_id, task_runner_task=task_result, tenant_id=tenant_id
)
return task_result
def test_skip_when_scheduled_scan_executing(
self, tenants_fixture, providers_fixture
):
"""Skip a scheduled run when another scheduled scan is already executing."""
tenant = tenants_fixture[0]
provider = providers_fixture[0]
periodic_task = self._create_periodic_task(provider.id, tenant.id)
task_id = str(uuid.uuid4())
self._create_task_result(tenant.id, task_id)
executing_scan = Scan.objects.create(
tenant_id=tenant.id,
provider=provider,
name="Daily scheduled scan",
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.EXECUTING,
scheduler_task_id=periodic_task.id,
)
with (
patch("tasks.tasks.perform_prowler_scan") as mock_scan,
patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks,
self._override_task_request(perform_scheduled_scan_task, id=task_id),
):
result = perform_scheduled_scan_task.run(
tenant_id=str(tenant.id), provider_id=str(provider.id)
)
mock_scan.assert_not_called()
mock_complete_tasks.assert_not_called()
assert result["id"] == str(executing_scan.id)
assert result["state"] == StateChoices.EXECUTING
assert (
Scan.objects.filter(
tenant_id=tenant.id,
provider=provider,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
).count()
== 0
)
def test_creates_next_scheduled_scan_after_completion(
self, tenants_fixture, providers_fixture
):
"""Create a next scheduled scan after a successful run completes."""
tenant = tenants_fixture[0]
provider = providers_fixture[0]
self._create_periodic_task(provider.id, tenant.id)
task_id = str(uuid.uuid4())
self._create_task_result(tenant.id, task_id)
def _complete_scan(tenant_id, scan_id, provider_id):
other_scheduled = Scan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
).exclude(id=scan_id)
assert not other_scheduled.exists()
scan_instance = Scan.objects.get(id=scan_id)
scan_instance.state = StateChoices.COMPLETED
scan_instance.save()
return {"status": "ok"}
with (
patch("tasks.tasks.perform_prowler_scan", side_effect=_complete_scan),
patch("tasks.tasks._perform_scan_complete_tasks"),
self._override_task_request(perform_scheduled_scan_task, id=task_id),
):
perform_scheduled_scan_task.run(
tenant_id=str(tenant.id), provider_id=str(provider.id)
)
scheduled_scans = Scan.objects.filter(
tenant_id=tenant.id,
provider=provider,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
)
assert scheduled_scans.count() == 1
assert scheduled_scans.first().scheduled_at > datetime.now(timezone.utc)
assert (
Scan.objects.filter(
tenant_id=tenant.id,
provider=provider,
trigger=Scan.TriggerChoices.SCHEDULED,
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
).count()
== 1
)
assert (
Scan.objects.filter(
tenant_id=tenant.id,
provider=provider,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.COMPLETED,
).count()
== 1
)
def test_dedupes_multiple_scheduled_scans_before_run(
self, tenants_fixture, providers_fixture
):
"""Ensure duplicated scheduled scans are removed before executing."""
tenant = tenants_fixture[0]
provider = providers_fixture[0]
periodic_task = self._create_periodic_task(provider.id, tenant.id)
task_id = str(uuid.uuid4())
self._create_task_result(tenant.id, task_id)
scheduled_scan = Scan.objects.create(
tenant_id=tenant.id,
provider=provider,
name="Daily scheduled scan",
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
scheduled_at=datetime.now(timezone.utc),
scheduler_task_id=periodic_task.id,
)
duplicate_scan = Scan.objects.create(
tenant_id=tenant.id,
provider=provider,
name="Daily scheduled scan",
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.AVAILABLE,
scheduled_at=scheduled_scan.scheduled_at,
scheduler_task_id=periodic_task.id,
)
def _complete_scan(tenant_id, scan_id, provider_id):
other_scheduled = Scan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
).exclude(id=scan_id)
assert not other_scheduled.exists()
scan_instance = Scan.objects.get(id=scan_id)
scan_instance.state = StateChoices.COMPLETED
scan_instance.save()
return {"status": "ok"}
with (
patch("tasks.tasks.perform_prowler_scan", side_effect=_complete_scan),
patch("tasks.tasks._perform_scan_complete_tasks"),
self._override_task_request(perform_scheduled_scan_task, id=task_id),
):
perform_scheduled_scan_task.run(
tenant_id=str(tenant.id), provider_id=str(provider.id)
)
assert not Scan.objects.filter(id=duplicate_scan.id).exists()
assert Scan.objects.filter(id=scheduled_scan.id).exists()
assert (
Scan.objects.filter(
tenant_id=tenant.id,
provider=provider,
trigger=Scan.TriggerChoices.SCHEDULED,
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
).count()
== 1
)
+59
View File
@@ -5,6 +5,10 @@ from enum import Enum
from django_celery_beat.models import PeriodicTask
from django_celery_results.models import TaskResult
from api.models import Scan, StateChoices
SCHEDULED_SCAN_NAME = "Daily scheduled scan"
class CustomEncoder(json.JSONEncoder):
def default(self, o):
@@ -71,3 +75,58 @@ def batched(iterable, batch_size):
batch = []
yield batch, True
def _get_or_create_scheduled_scan(
tenant_id: str,
provider_id: str,
scheduler_task_id: int,
scheduled_at: datetime,
update_state: bool = False,
) -> Scan:
"""
Get or create a scheduled scan, cleaning up duplicates if found.
Args:
tenant_id: The tenant ID.
provider_id: The provider ID.
scheduler_task_id: The PeriodicTask ID.
scheduled_at: The scheduled datetime for the scan.
update_state: If True, also reset state to SCHEDULED when updating.
Returns:
The scan instance to use.
"""
scheduled_scans = list(
Scan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
scheduler_task_id=scheduler_task_id,
).order_by("scheduled_at", "inserted_at")
)
if scheduled_scans:
scan_instance = scheduled_scans[0]
if len(scheduled_scans) > 1:
Scan.objects.filter(id__in=[s.id for s in scheduled_scans[1:]]).delete()
needs_update = scan_instance.scheduled_at != scheduled_at
if update_state and scan_instance.state != StateChoices.SCHEDULED:
scan_instance.state = StateChoices.SCHEDULED
scan_instance.name = SCHEDULED_SCAN_NAME
needs_update = True
if needs_update:
scan_instance.scheduled_at = scheduled_at
scan_instance.save()
return scan_instance
return Scan.objects.create(
tenant_id=tenant_id,
name=SCHEDULED_SCAN_NAME,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
scheduled_at=scheduled_at,
scheduler_task_id=scheduler_task_id,
)
+1 -1
View File
@@ -98,7 +98,7 @@ services:
# Auth
- NEO4J_AUTH=${NEO4J_USER}/${NEO4J_PASSWORD}
# Memory limits
- NEO4J_dbms_max__databases=${NEO4J_DBMS_MAX__DATABASES:-1000000}
- NEO4J_dbms_max__databases=${NEO4J_DBMS_MAX__DATABASES:-1000}
- NEO4J_server_memory_pagecache_size=${NEO4J_SERVER_MEMORY_PAGECACHE_SIZE:-1G}
- NEO4J_server_memory_heap_initial__size=${NEO4J_SERVER_MEMORY_HEAP_INITIAL__SIZE:-1G}
- NEO4J_server_memory_heap_max__size=${NEO4J_SERVER_MEMORY_HEAP_MAX__SIZE:-1G}
+1 -1
View File
@@ -84,7 +84,7 @@ services:
# Auth
- NEO4J_AUTH=${NEO4J_USER}/${NEO4J_PASSWORD}
# Memory limits
- NEO4J_dbms_max__databases=${NEO4J_DBMS_MAX__DATABASES:-1000000}
- NEO4J_dbms_max__databases=${NEO4J_DBMS_MAX__DATABASES:-1000}
- NEO4J_server_memory_pagecache_size=${NEO4J_SERVER_MEMORY_PAGECACHE_SIZE:-1G}
- NEO4J_server_memory_heap_initial__size=${NEO4J_SERVER_MEMORY_HEAP_INITIAL__SIZE:-1G}
- NEO4J_server_memory_heap_max__size=${NEO4J_SERVER_MEMORY_HEAP_MAX__SIZE:-1G}
-11
View File
@@ -1,11 +0,0 @@
---
title: 'Contact Us'
---
For technical support or any type of inquiries, you are very welcome to:
- Reach out to community members on the [**Prowler Slack channel**](https://goto.prowler.com/slack)
- Open an Issue or a Pull Request in our [**GitHub repository**](https://github.com/prowler-cloud/prowler).
We will appreciate all types of feedback and contribution, Prowler would not be the same without our vibrant community! 😃
+22 -4
View File
@@ -331,14 +331,28 @@
},
{
"tab": "Security",
"pages": [
"security"
"groups": [
{
"group": "Security & Compliance",
"pages": [
"security/index",
"security/software-security"
]
},
{
"group": "Prowler Cloud",
"pages": [
"security/encryption",
"security/data-regions",
"security/networking"
]
}
]
},
{
"tab": "Contact Us",
"tab": "Support",
"pages": [
"contact"
"support"
]
},
{
@@ -459,6 +473,10 @@
{
"source": "/projects/prowler-open-source/en/latest/tutorials/:slug*",
"destination": "/user-guide/tutorials/:slug*"
},
{
"source": "/contact",
"destination": "/support"
}
]
}
@@ -115,8 +115,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.16.0"
PROWLER_API_VERSION="5.16.0"
PROWLER_UI_VERSION="5.17.0"
PROWLER_API_VERSION="5.17.0"
```
<Note>
-163
View File
@@ -1,163 +0,0 @@
---
title: 'Security'
---
## Compliance and Trust
We publish our live SOC 2 Type 2 Compliance data at [https://trust.prowler.com](https://trust.prowler.com)
As an **AWS Partner**, we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/).
## Encryption (Prowler Cloud)
We use encryption everywhere possible. The data and communications used by **Prowler Cloud** are **encrypted at-rest** and **in-transit**.
## Data Retention Policy (Prowler Cloud)
Prowler Cloud is GDPR compliant in regards to personal data and the ["right to be forgotten"](https://gdpr.eu/right-to-be-forgotten/). When a user deletes their account their user information will be deleted from Prowler Cloud online and backup systems within 10 calendar days.
## Software Security
We follow a **security-by-design approach** throughout our software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment.
We enforce [pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations to catch issues early, and [our CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates to ensure code quality, secure configurations, and compliance with internal standards.
Our container registries are continuously scanned for vulnerabilities, with findings automatically reported to our security team for assessment and remediation. This process evolves alongside our stack as we adopt new languages, frameworks, and technologies, ensuring our security practices remain comprehensive, proactive, and adaptable.
### Static Application Security Testing (SAST)
We employ multiple SAST tools across our codebase to identify security vulnerabilities, code quality issues, and potential bugs during development:
#### CodeQL Analysis
- **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 Security Scanners
- **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
#### Code Quality & Dead Code Detection
- **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)
We continuously monitor our dependencies for known vulnerabilities and ensure timely updates:
#### Dependency Vulnerability Scanning
- **Safety**: Scans Python dependencies against known vulnerability databases
- Runs on every commit via pre-commit hooks
- Integrated into CI/CD for SDK and API
- Configured with selective ignores for tracked exceptions
- **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
#### Automated Dependency Updates
- **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
All container images are scanned before deployment:
- **Trivy Vulnerability Scanning**:
- 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
- **Hadolint**: Dockerfile linting to enforce best practices
- Validates Dockerfile syntax and structure
- Ensures secure image building practices
### Secrets Detection
We protect 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 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
## Reporting Vulnerabilities
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems.
When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope:
- Social engineering support or attacks requiring social engineering.
- Clickjacking on pages with no sensitive actions.
- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions.
- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device.
- Previously known vulnerable libraries without a working Proof of Concept (PoC).
- Comma Separated Values (CSV) injection without demonstrating a vulnerability.
- Missing best practices in SSL/TLS configuration.
- Any activity that could lead to the disruption of service (DoS).
- Rate limiting or brute force issues on non-authentication endpoints.
- Missing best practices in Content Security Policy (CSP).
- Missing HttpOnly or Secure flags on cookies.
- Configuration of or missing security headers.
- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records.
- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind).
- Software version disclosure, banner identification issues, or descriptive error messages.
- Tabnabbing.
- Issues that require unlikely user interaction.
- Improper logout functionality and improper session timeout.
- CORS misconfiguration without an exploitation scenario.
- Broken link hijacking.
- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified.
- Content spoofing and text injection issues without a clear attack vector.
- Email spoofing without exploiting security flaws.
- Dead links or broken links.
- User enumeration.
Testing guidelines:
- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers.
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
Reporting guidelines:
- File a report through our Support Desk at https://support.prowler.com
- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack.
Disclosure guidelines:
- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers.
- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included:
- Data regarding any Prowler user or customer projects.
- Prowler customers' data.
- Information about Prowler employees, contractors or partners.
What we promise:
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
- We will keep you informed of the progress towards resolving the problem.
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise).
We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
+25
View File
@@ -0,0 +1,25 @@
---
title: 'Data Regions & Availability'
---
Prowler Cloud runs on AWS with high availability built in.
## Regions
| Region | URL | Location |
|--------|-----|----------|
| **EU** | [cloud.prowler.com](https://cloud.prowler.com) | Ireland (`eu-west-1`) |
## Business Continuity
| Control | Details |
|---------|---------|
| **High Availability** | Multi-AZ databases and load-balanced stateless application layer on AWS |
| **Disaster Recovery** | Encrypted backups, tested regularly |
| **[RPO](https://en.wikipedia.org/wiki/Recovery_point_objective)** | 24 hours |
| **[RTO](https://en.wikipedia.org/wiki/Recovery_time_objective)** | 2 hours |
| **Status** | [status.prowler.com](https://status.prowler.com) — uptime history and incidents |
## Contact
For questions about data regions and availability, visit the [Support page](/support).
+25
View File
@@ -0,0 +1,25 @@
---
title: 'Encryption'
---
Prowler Cloud uses encryption everywhere possible. All data and communications are encrypted at rest and in transit.
## Encryption at Rest
All data stored in Prowler Cloud is encrypted at rest using AES-256 encryption, including:
- **Database contents:** All scan results, findings, and configuration data.
- **File storage:** Reports, exports, and uploaded files.
- **Backups:** All backup data is encrypted.
## Encryption in Transit
All communications with Prowler Cloud are encrypted in transit using TLS 1.2 or higher, including:
- **API requests:** All REST API communications.
- **Web application traffic:** Browser-to-server connections.
- **Internal service communication:** Service-to-service traffic within the platform.
## Contact
For questions regarding encryption, visit the [Support page](/support).
+76
View File
@@ -0,0 +1,76 @@
---
title: 'Security & Compliance'
---
**Prowler secures itself with Prowler.** As an open-source cloud security platform trusted by thousands of organizations, Prowler applies the same rigorous security standards internally that customers achieve externally.
All security tooling, configurations, and CI/CD pipelines are publicly available in the [Prowler GitHub repository](https://github.com/prowler-cloud/prowler). Transparency is fundamental to open-source security.
## Software Security
All Prowler code goes through the same security pipeline, whether running on Prowler Cloud or self-managed infrastructure: DAST, SAST, SCA, container scanning, and secrets detection on every build.
<Card title="Software Security" icon="code" href="/security/software-security">
Security tools and practices applied to all Prowler code.
</Card>
## Prowler Cloud vs Self-Managed
| | Prowler Cloud | Self-Managed |
|--|---------------|--------------|
| **Deployment** | Fully managed SaaS | Own infrastructure |
| **Region** | EU (Ireland) | Any region or provider |
| **Compliance** | SOC 2 Type II, AWS FTR | Organization responsibility |
| **Data Control** | Prowler managed | Full control |
| **Encryption** | AES-256 at rest, TLS 1.2+ in transit | Configurable |
| **Backups** | Automated | Organization responsibility |
| **Updates** | Automatic | Manual |
<Note>
Self-Managed includes Prowler App and Prowler CLI. They can run anywhere — any cloud provider, any region, on-premises, or air-gapped environments. Full control over data residency and infrastructure decisions. See the [Prowler App Installation Guide](/getting-started/installation/prowler-app) to get started.
</Note>
---
## Prowler Cloud
This section covers security and compliance for **Prowler Cloud**, the managed infrastructure.
### Trust & Compliance
Prowler Cloud holds compliance certifications and undergoes regular audits.
| Certification | Status |
|---------------|--------|
| **SOC 2 Type II** | [View on Trust Portal](https://trust.prowler.com) |
| **AWS Foundational Technical Review (FTR)** | Passed — [Details](https://aws.amazon.com/partners/foundational-technical-review/) |
Compliance data and reports: [trust.prowler.com](https://trust.prowler.com)
### Security
<Columns cols={3}>
<Card title="Encryption" icon="lock" href="/security/encryption">
Data encrypted at rest (AES-256) and in transit (TLS 1.2+).
</Card>
<Card title="Data Regions" icon="globe" href="/security/data-regions">
EU-hosted infrastructure with high availability and disaster recovery.
</Card>
<Card title="Networking" icon="network-wired" href="/security/networking">
Static egress IPs for firewall allowlisting.
</Card>
</Columns>
### Privacy
Prowler Cloud is GDPR compliant in regard to the ["right to be forgotten"](https://gdpr.eu/right-to-be-forgotten/). When an account is deleted, user information is removed from online and backup systems within 10 calendar days.
---
## Report a Vulnerability
Found a security issue? Report it through the [responsible disclosure](https://prowler.com/.well-known/security.txt) process.
## Contact
For security inquiries or general support, visit the [Support page](/support).
+21
View File
@@ -0,0 +1,21 @@
---
title: 'Networking'
---
## Egress IP Addresses
Prowler Cloud makes outbound API calls to scan cloud provider accounts and connect to integrations. Allowlist these IPs in firewalls or security groups to restrict access to Prowler Cloud only.
| Region | IP Address |
|--------|------------|
| EU (Ireland) | `52.48.254.174` |
Resolve the egress IP via DNS:
```bash
dig egress.prowler.com +short
```
## Contact
For questions about networking, visit the [Support page](/support).
+97
View File
@@ -0,0 +1,97 @@
---
title: 'Software Security'
---
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.
[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.
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 are employed across the codebase to identify security vulnerabilities, code quality issues, and potential bugs during development.
### CodeQL Analysis
- **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 Security Scanners
- **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
### Code Quality & Dead Code Detection
- **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 continuously monitored for known vulnerabilities with timely updates ensured.
### Dependency Vulnerability Scanning
- **Safety:** Scans Python dependencies against known vulnerability databases
- Runs on every commit via pre-commit hooks
- Integrated into CI/CD for SDK and API
- Configured with selective ignores for tracked exceptions
- **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
### Automated Dependency Updates
- **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
All container images are scanned before deployment.
### Trivy Vulnerability Scanning
- 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
### Hadolint
- Validates Dockerfile syntax and structure
- Ensures secure image building practices
## Secrets Detection
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 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 regarding software security, visit the [Support page](/support).
+62
View File
@@ -0,0 +1,62 @@
---
title: 'Support'
description: 'Get help with Prowler'
---
## Lighthouse AI
Lighthouse AI is a Cloud Security Analyst chatbot powered by [Prowler MCP](/getting-started/products/prowler-mcp), your 24/7 virtual cloud security analyst. It can:
- **Query your security data**: Findings, compliance status, resources, and remediation guidance
- **Search Prowler Hub**: Over 1,000 security checks and 70+ compliance frameworks
- **Access documentation**: Search and retrieve Prowler docs contextually
Available in Prowler Cloud and Prowler App.
[Learn more about Lighthouse AI](/getting-started/products/prowler-lighthouse-ai)
## Support Desk
> Available to **Prowler Cloud** customers.
For Prowler Cloud customers, submit support requests through our support desk. We'll route your request to the right team and respond via email.
<Card title="Submit a request" icon="ticket" href="https://support.prowler.com">
Contact our support team
</Card>
## GitHub Discussions
Prowler is Open Source. If you have a question, it's likely someone else has it too. We'd love to answer in the open on GitHub whenever possible.
<CardGroup cols={3}>
<Card title="Ask a question" icon="circle-question" href="https://github.com/prowler-cloud/prowler/discussions">
Get help from the community
</Card>
<Card title="Report a bug" icon="bug" href="https://github.com/prowler-cloud/prowler/issues/new?template=bug_report.yml">
Found something wrong? Let us know
</Card>
<Card title="Suggest a feature" icon="lightbulb" href="https://github.com/prowler-cloud/prowler/issues/new?template=feature-request.yml">
Share your ideas for improvements
</Card>
</CardGroup>
## Community Slack
Join our Slack workspace to connect with the Prowler community, ask questions, and get help from other users and the Prowler team.
<Card title="Join Prowler Slack" icon="slack" href="https://goto.prowler.com/slack">
Connect with the community
</Card>
## Office Hours
Join our open calls to discuss what you're building, ask questions, and connect with the Prowler team and community.
Office Hours sessions are announced on [LinkedIn](https://www.linkedin.com/company/prowler-security/). Recordings of previous sessions are available on [YouTube](https://www.youtube.com/playlist?list=PLIwvjRXuMGkE-BDYXmUR2TXYQ7agxtuB1).
## Security
To report a vulnerability or for security-related inquiries, contact [security@prowler.com](mailto:security@prowler.com).
See also: [Responsible Disclosure](https://prowler.com/.well-known/security.txt)
+2
View File
@@ -9,7 +9,9 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Action | Skill |
|--------|-------|
| Add changelog entry for a PR or feature | `prowler-changelog` |
| Committing changes | `prowler-commit` |
| Create PR that requires changelog entry | `prowler-changelog` |
| Creating a git commit | `prowler-commit` |
| Review changelog format and conventions | `prowler-changelog` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Working on MCP server tools | `prowler-mcp` |
Generated
+129 -73
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -741,7 +741,7 @@ version = "4.9.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
@@ -1546,8 +1546,8 @@ files = [
jmespath = ">=0.7.1,<2.0.0"
python-dateutil = ">=2.1,<3.0.0"
urllib3 = [
{version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""},
{version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""},
{version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""},
]
[package.extras]
@@ -2378,20 +2378,29 @@ testing = ["hatch", "pre-commit", "pytest", "tox"]
[[package]]
name = "filelock"
version = "3.12.4"
version = "3.19.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "dev"]
markers = "python_version < \"3.10\""
files = [
{file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
{file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
{file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"},
{file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
typing = ["typing-extensions (>=4.7.1) ; python_version < \"3.11\""]
[[package]]
name = "filelock"
version = "3.20.3"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
markers = "python_version >= \"3.10\""
files = [
{file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
{file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
]
[[package]]
name = "flake8"
@@ -2706,7 +2715,7 @@ version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
@@ -2746,7 +2755,7 @@ version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
@@ -2783,7 +2792,7 @@ version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
@@ -2964,6 +2973,18 @@ files = [
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
[[package]]
name = "joblib"
version = "1.5.3"
description = "Lightweight pipelining with Python functions"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"},
{file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"},
]
[[package]]
name = "joserfc"
version = "1.2.2"
@@ -3927,6 +3948,32 @@ extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.1
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"]
test-extras = ["pytest-mpl", "pytest-randomly"]
[[package]]
name = "nltk"
version = "3.9.2"
description = "Natural Language Toolkit"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a"},
{file = "nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419"},
]
[package.dependencies]
click = "*"
joblib = "*"
regex = ">=2021.8.3"
tqdm = "*"
[package.extras]
all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"]
corenlp = ["requests"]
machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"]
plot = ["matplotlib"]
tgrep = ["pyparsing"]
twitter = ["twython"]
[[package]]
name = "nodeenv"
version = "1.9.1"
@@ -4489,36 +4536,6 @@ files = [
{file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"},
]
[[package]]
name = "psutil"
version = "6.0.0"
description = "Cross-platform lib for process and system monitoring in Python."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["dev"]
files = [
{file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"},
{file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"},
{file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"},
{file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"},
{file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"},
{file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"},
{file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"},
{file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"},
{file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"},
{file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"},
{file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"},
{file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"},
{file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"},
{file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"},
{file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"},
{file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"},
{file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"},
]
[package.extras]
test = ["enum34 ; python_version <= \"3.4\"", "ipaddress ; python_version < \"3.0\"", "mock ; python_version < \"3.0\"", "pywin32 ; sys_platform == \"win32\"", "wmi ; sys_platform == \"win32\""]
[[package]]
name = "py-iam-expand"
version = "0.1.0"
@@ -4568,14 +4585,14 @@ dev = ["black (==22.6.0)", "flake8", "mypy", "pytest"]
[[package]]
name = "pyasn1"
version = "0.6.1"
version = "0.6.2"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
{file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"},
{file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"},
]
[[package]]
@@ -4612,7 +4629,7 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
markers = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@@ -5675,34 +5692,35 @@ crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
[[package]]
name = "safety"
version = "3.2.9"
description = "Checks installed dependencies for known vulnerabilities and licenses."
version = "3.7.0"
description = "Scan dependencies for known vulnerabilities and licenses."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "safety-3.2.9-py3-none-any.whl", hash = "sha256:5e199c057550dc6146c081084274279dfb98c17735193b028db09a55ea508f1a"},
{file = "safety-3.2.9.tar.gz", hash = "sha256:494bea752366161ac9e0742033d2a82e4dc51d7c788be42e0ecf5f3ef36b8071"},
{file = "safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf"},
{file = "safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e"},
]
[package.dependencies]
Authlib = ">=1.2.0"
Click = ">=8.0.2"
dparse = ">=0.6.4b0"
filelock = ">=3.12.2,<3.13.0"
authlib = ">=1.2.0"
click = ">=8.0.2"
dparse = ">=0.6.4"
filelock = ">=3.16.1,<4.0"
httpx = "*"
jinja2 = ">=3.1.0"
marshmallow = ">=3.15.0"
nltk = ">=3.9"
packaging = ">=21.0"
psutil = ">=6.0.0,<6.1.0"
pydantic = ">=1.10.12"
pydantic = ">=2.6.0"
requests = "*"
rich = "*"
"ruamel.yaml" = ">=0.17.21"
safety-schemas = ">=0.0.4"
setuptools = ">=65.5.1"
typer = "*"
ruamel-yaml = ">=0.17.21"
safety-schemas = "0.0.16"
tenacity = ">=8.1.0"
tomli = {version = "*", markers = "python_version < \"3.11\""}
tomlkit = "*"
typer = ">=0.16.0"
typing-extensions = ">=4.7.1"
urllib3 = ">=1.26.5"
[package.extras]
github = ["pygithub (>=1.43.3)"]
@@ -5711,20 +5729,20 @@ spdx = ["spdx-tools (>=0.8.2)"]
[[package]]
name = "safety-schemas"
version = "0.0.5"
version = "0.0.16"
description = "Schemas for Safety tools"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "safety_schemas-0.0.5-py3-none-any.whl", hash = "sha256:6ac9eb71e60f0d4e944597c01dd48d6d8cd3d467c94da4aba3702a05a3a6ab4f"},
{file = "safety_schemas-0.0.5.tar.gz", hash = "sha256:0de5fc9a53d4423644a8ce9a17a2e474714aa27e57f3506146e95a41710ff104"},
{file = "safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44"},
{file = "safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e"},
]
[package.dependencies]
dparse = ">=0.6.4b0"
dparse = ">=0.6.4"
packaging = ">=21.0"
pydantic = "*"
pydantic = ">=2.6.0"
ruamel-yaml = ">=0.17.21"
typing-extensions = ">=4.7.1"
@@ -5828,7 +5846,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
groups = ["main", "dev"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -5894,6 +5912,22 @@ files = [
[package.extras]
widechars = ["wcwidth"]
[[package]]
name = "tenacity"
version = "9.1.2"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"},
{file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"},
]
[package.extras]
doc = ["reno", "sphinx"]
test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]]
name = "tldextract"
version = "5.3.0"
@@ -5971,6 +6005,28 @@ files = [
{file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"},
]
[[package]]
name = "tqdm"
version = "4.67.1"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
{file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"]
discord = ["requests"]
notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[[package]]
name = "typer"
version = "0.16.0"
@@ -6558,4 +6614,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "3a52abdf28e0895fb05281d4febc871302cb1a9c121acec1139b1135d11756d4"
content-hash = "adfc2da2c6e3e803f7a151b9697dbc3f461366a03e4504eb97498cbc72b2e48c"
+84 -15
View File
@@ -2,11 +2,34 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.17.0] (Prowler UNRELEASED)
## [5.18.0] (Prowler UNRELEASED)
### Added
- `defender_zap_for_teams_enabled` check for M365 provider [(#9838)](https://github.com/prowler-cloud/prowler/pull/9838)
- `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747)
- `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840)
- `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828)
### Changed
- Update Azure App Service service metadata to new format [(#9613)](https://github.com/prowler-cloud/prowler/pull/9613)
- Update Azure Application Insights service metadata to new format [(#9614)](https://github.com/prowler-cloud/prowler/pull/9614)
- Update Azure Container Registry service metadata to new format [(#9615)](https://github.com/prowler-cloud/prowler/pull/9615)
- Update Azure Cosmos DB service metadata to new format [(#9616)](https://github.com/prowler-cloud/prowler/pull/9616)
- Update Azure Databricks service metadata to new format [(#9617)](https://github.com/prowler-cloud/prowler/pull/9617)
- Parallelize Azure Key Vault vaults and vaults contents retrieval to improve performance [(#9876)](https://github.com/prowler-cloud/prowler/pull/9876)
- Update Azure IAM service metadata to new format [(#9620)](https://github.com/prowler-cloud/prowler/pull/9620)
- Update Azure Policy service metadata to new format [(#9625)](https://github.com/prowler-cloud/prowler/pull/9625)
---
## [5.17.0] (Prowler v5.17.0)
### Added
- AI Skills pack for AI coding assistants (Claude Code, OpenCode, Codex) following agentskills.io standard [(#9728)](https://github.com/prowler-cloud/prowler/pull/9728)
- Add Prowler ThreatScore for the Alibaba Cloud provider [(#9511)](https://github.com/prowler-cloud/prowler/pull/9511)
- Prowler ThreatScore for the Alibaba Cloud provider [(#9511)](https://github.com/prowler-cloud/prowler/pull/9511)
- `compute_instance_group_multiple_zones` check for GCP provider [(#9566)](https://github.com/prowler-cloud/prowler/pull/9566)
- `compute_instance_group_autohealing_enabled` check for GCP provider [(#9690)](https://github.com/prowler-cloud/prowler/pull/9690)
- Support AWS European Sovereign Cloud [(#9649)](https://github.com/prowler-cloud/prowler/pull/9649)
@@ -16,16 +39,19 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `compute_configuration_changes` check for GCP provider to detect Compute Engine configuration changes in Cloud Audit Logs [(#9698)](https://github.com/prowler-cloud/prowler/pull/9698)
- `compute_instance_group_load_balancer_attached` check for GCP provider [(#9695)](https://github.com/prowler-cloud/prowler/pull/9695)
- `Cloudflare` provider with critical security checks [(#9423)](https://github.com/prowler-cloud/prowler/pull/9423)
- CloudFlare `TLS/SSL`, `records` and `email` checks for `zone` service [(#9424)](https://github.com/prowler-cloud/prowler/pull/9424)
- `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702)
- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)
- `TLS/SSL`, `records` and `email` checks for `zone` service [(#9424)](https://github.com/prowler-cloud/prowler/pull/9424)
- `compute_snapshot_not_outdated` check for GCP provider [(#9774)](https://github.com/prowler-cloud/prowler/pull/9774)
- `compute_project_os_login_2fa_enabled` check for GCP provider [(#9839)](https://github.com/prowler-cloud/prowler/pull/9839)
- `compute_instance_on_host_maintenance_migrate` check for GCP provider [(#9834)](https://github.com/prowler-cloud/prowler/pull/9834)
- CIS 1.12 compliance framework for Kubernetes [(#9778)](https://github.com/prowler-cloud/prowler/pull/9778)
- CIS 6.0 for M365 provider [(#9779)](https://github.com/prowler-cloud/prowler/pull/9779)
- CIS 5.0 compliance framework for the Azure provider [(#9777)](https://github.com/prowler-cloud/prowler/pull/9777)
- `Cloudflare` Bot protection, WAF, Privacy, Anti-Scraping and Zone configuration checks [(#9425)](https://github.com/prowler-cloud/prowler/pull/9425)
### Changed
- Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432)
- Update AWS Route 53 service metadata to new format [(#9406)](https://github.com/prowler-cloud/prowler/pull/9406)
- Update AWS SQS service metadata to new format [(#9429)](https://github.com/prowler-cloud/prowler/pull/9429)
@@ -54,20 +80,25 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Enhance `user_registration_details` perfomance and user `mfa` evaluation [(#9236)](https://github.com/prowler-cloud/prowler/pull/9236)
- Update AWS Cognito service metadata to new format [(#8853)](https://github.com/prowler-cloud/prowler/pull/8853)
- Update AWS EC2 service metadata to new format [(#9549)](https://github.com/prowler-cloud/prowler/pull/9549)
---
## [5.16.2] (Prowler v5.16.2) (UNRELEASED)
- Update Azure AI Search service metadata to new format [(#9087)](https://github.com/prowler-cloud/prowler/pull/9087)
- Update Azure AKS service metadata to new format [(#9611)](https://github.com/prowler-cloud/prowler/pull/9611)
- Update Azure API Management service metadata to new format [(#9612)](https://github.com/prowler-cloud/prowler/pull/9612)
### Fixed
- Fix OCI authentication error handling and validation [(#9738)](https://github.com/prowler-cloud/prowler/pull/9738)
- Fixup AWS EC2 SG library [(#9216)](https://github.com/prowler-cloud/prowler/pull/9216)
- OCI authentication error handling and validation [(#9738)](https://github.com/prowler-cloud/prowler/pull/9738)
- AWS EC2 SG library [(#9216)](https://github.com/prowler-cloud/prowler/pull/9216)
### Security
- `safety` to `3.7.0` and `filelock` to `3.20.3` due to [Safety vulnerability 82754 (CVE-2025-68146)](https://data.safetycli.com/v/82754/97c/) [(#9816)](https://github.com/prowler-cloud/prowler/pull/9816)
- `pyasn1` to v0.6.2 to address [CVE-2026-23490](https://nvd.nist.gov/vuln/detail/CVE-2026-23490) [(#9817)](https://github.com/prowler-cloud/prowler/pull/9817)
---
## [5.16.1] (Prowler v5.16.1)
### Fixed
- ZeroDivision error from Prowler ThreatScore [(#9653)](https://github.com/prowler-cloud/prowler/pull/9653)
---
@@ -75,10 +106,12 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.16.0] (Prowler v5.16.0)
### Added
- `privilege-escalation` and `ec2-imdsv1` categories for AWS checks [(#9537)](https://github.com/prowler-cloud/prowler/pull/9537)
- Supported IaC formats and scanner documentation for the IaC provider [(#9553)](https://github.com/prowler-cloud/prowler/pull/9553)
### Changed
- Update AWS Glue service metadata to new format [(#9258)](https://github.com/prowler-cloud/prowler/pull/9258)
- Update AWS Kafka service metadata to new format [(#9261)](https://github.com/prowler-cloud/prowler/pull/9261)
- Update AWS KMS service metadata to new format [(#9263)](https://github.com/prowler-cloud/prowler/pull/9263)
@@ -91,6 +124,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS WAF v2 service metadata to new format [(#9481)](https://github.com/prowler-cloud/prowler/pull/9481)
### Fixed
- Fix typo `trustboundaries` category to `trust-boundaries` [(#9536)](https://github.com/prowler-cloud/prowler/pull/9536)
- Fix incorrect `bedrock-agent` regional availability, now using official AWS docs instead of copying from `bedrock`
- Store MongoDB Atlas provider regions as lowercase [(#9554)](https://github.com/prowler-cloud/prowler/pull/9554)
@@ -101,6 +135,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.15.1] (Prowler v5.15.1)
### Fixed
- Fix false negative in AWS `apigateway_restapi_logging_enabled` check by refining stage logging evaluation to ensure logging level is not set to "OFF" [(#9304)](https://github.com/prowler-cloud/prowler/pull/9304)
---
@@ -108,6 +143,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.15.0] (Prowler v5.15.0)
### Added
- `cloudstorage_uses_vpc_service_controls` check for GCP provider [(#9256)](https://github.com/prowler-cloud/prowler/pull/9256)
- Alibaba Cloud provider with CIS 2.0 benchmark [(#9329)](https://github.com/prowler-cloud/prowler/pull/9329)
- `repository_immutable_releases_enabled` check for GitHub provider [(#9162)](https://github.com/prowler-cloud/prowler/pull/9162)
@@ -121,6 +157,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- RBI Cyber Security Framework compliance for Azure provider [(#8822)](https://github.com/prowler-cloud/prowler/pull/8822)
### Changed
- Update AWS Macie service metadata to new format [(#9265)](https://github.com/prowler-cloud/prowler/pull/9265)
- Update AWS Lightsail service metadata to new format [(#9264)](https://github.com/prowler-cloud/prowler/pull/9264)
- Update AWS GuardDuty service metadata to new format [(#9259)](https://github.com/prowler-cloud/prowler/pull/9259)
@@ -130,6 +167,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS Lightsail service metadata to new format [(#9264)](https://github.com/prowler-cloud/prowler/pull/9264)
### Fixed
- Fix duplicate requirement IDs in ISO 27001:2013 AWS compliance framework by adding unique letter suffixes
- Removed incorrect threat-detection category from checks metadata [(#9489)](https://github.com/prowler-cloud/prowler/pull/9489)
- GCP `cloudstorage_uses_vpc_service_controls` check to handle VPC Service Controls blocked API access [(#9478)](https://github.com/prowler-cloud/prowler/pull/9478)
@@ -139,6 +177,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.14.2] (Prowler v5.14.2)
### Fixed
- Custom check folder metadata validation [(#9335)](https://github.com/prowler-cloud/prowler/pull/9335)
- Pin `alibabacloud-gateway-oss-util` to version 0.0.3 to address missing dependency [(#9487)](https://github.com/prowler-cloud/prowler/pull/9487)
@@ -147,6 +186,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.14.1] (Prowler v5.14.1)
### Fixed
- `sharepoint_external_sharing_managed` check to handle external sharing disabled at organization level [(#9298)](https://github.com/prowler-cloud/prowler/pull/9298)
- Support multiple Exchange mailbox policies in M365 `exchange_mailbox_policy_additional_storage_restricted` check [(#9241)](https://github.com/prowler-cloud/prowler/pull/9241)
@@ -155,6 +195,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.14.0] (Prowler v5.14.0)
### Added
- GitHub provider check `organization_default_repository_permission_strict` [(#8785)](https://github.com/prowler-cloud/prowler/pull/8785)
- Add OCI mapping to scan and check classes [(#8927)](https://github.com/prowler-cloud/prowler/pull/8927)
- `codepipeline_project_repo_private` check for AWS provider [(#5915)](https://github.com/prowler-cloud/prowler/pull/5915)
@@ -180,6 +221,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add branch name to IaC provider region [(#9296)](https://github.com/prowler-cloud/prowler/pull/9295)
### Changed
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
- Update AWS DRS service metadata to new format [(#8870)](https://github.com/prowler-cloud/prowler/pull/8870)
- Update AWS DynamoDB service metadata to new format [(#8871)](https://github.com/prowler-cloud/prowler/pull/8871)
@@ -213,9 +255,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS ECS service metadata to new format [(#8888)](https://github.com/prowler-cloud/prowler/pull/8888)
- Update AWS Kinesis service metadata to new format [(#9262)](https://github.com/prowler-cloud/prowler/pull/9262)
- Update AWS DocumentDB service metadata to new format [(#8862)](https://github.com/prowler-cloud/prowler/pull/8862)
- Adapt IaC provider to be used in the Prowler App [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
### Fixed
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
- False negative in `iam_role_cross_service_confused_deputy_prevention` check [(#9213)](https://github.com/prowler-cloud/prowler/pull/9213)
@@ -233,6 +276,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.13.1] (Prowler v5.13.1)
### Fixed
- Add `resource_name` for checks under `logging` for the GCP provider [(#9023)](https://github.com/prowler-cloud/prowler/pull/9023)
- Fix `ec2_instance_with_outdated_ami` check to handle None AMIs [(#9046)](https://github.com/prowler-cloud/prowler/pull/9046)
- Handle timestamp when transforming compliance findings in CCC [(#9042)](https://github.com/prowler-cloud/prowler/pull/9042)
@@ -241,14 +285,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
### Changed
- Adapt IaC provider to be used in the Prowler App [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
---
## [5.13.0] (Prowler v5.13.0)
### Added
- Support for AdditionalURLs in outputs [(#8651)](https://github.com/prowler-cloud/prowler/pull/8651)
- Support for markdown metadata fields in Dashboard [(#8667)](https://github.com/prowler-cloud/prowler/pull/8667)
- `ec2_instance_with_outdated_ami` check for AWS provider [(#6910)](https://github.com/prowler-cloud/prowler/pull/6910)
@@ -291,6 +331,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
- Fix SNS topics showing empty AWS_ResourceID in Quick Inventory output [(#8762)](https://github.com/prowler-cloud/prowler/issues/8762)
- Fix HTML Markdown output for long strings [(#8803)](https://github.com/prowler-cloud/prowler/pull/8803)
- Prowler ThreatScore scoring calculation CLI [(#8582)](https://github.com/prowler-cloud/prowler/pull/8582)
@@ -307,6 +348,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.12.1] (Prowler v5.12.1)
### Fixed
- Replaced old check id with new ones for compliance files [(#8682)](https://github.com/prowler-cloud/prowler/pull/8682)
- `firehose_stream_encrypted_at_rest` check false positives and new api call in kafka service [(#8599)](https://github.com/prowler-cloud/prowler/pull/8599)
- Replace defender rules policies key to use old name [(#8702)](https://github.com/prowler-cloud/prowler/pull/8702)
@@ -316,6 +358,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.12.0] (Prowler v5.12.0)
### Added
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
- Support labels on Jira tickets [(#8603)](https://github.com/prowler-cloud/prowler/pull/8603)
- Add finding url and tenant info inside Jira tickets [(#8607)](https://github.com/prowler-cloud/prowler/pull/8607)
@@ -339,9 +382,11 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `projects_network_access_list_exposed_to_internet` - Ensure project network access list is not exposed to internet
### Changed
- Rename ftp and mongo checks to follow pattern `ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_*` [(#8293)](https://github.com/prowler-cloud/prowler/pull/8293)
### Fixed
- Renamed `AdditionalUrls` to `AdditionalURLs` field in CheckMetadata [(#8639)](https://github.com/prowler-cloud/prowler/pull/8639)
- TypeError from Python 3.9 in Security Hub module by updating type annotations [(#8619)](https://github.com/prowler-cloud/prowler/pull/8619)
- KeyError when SecurityGroups field is missing in MemoryDB check [(#8666)](https://github.com/prowler-cloud/prowler/pull/8666)
@@ -352,6 +397,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.11.0] (Prowler v5.11.0)
### Added
- Certificate authentication for M365 provider [(#8404)](https://github.com/prowler-cloud/prowler/pull/8404)
- `vm_sufficient_daily_backup_retention_period` check for Azure provider [(#8200)](https://github.com/prowler-cloud/prowler/pull/8200)
- `vm_jit_access_enabled` check for Azure provider [(#8202)](https://github.com/prowler-cloud/prowler/pull/8202)
@@ -366,10 +412,12 @@ All notable changes to the **Prowler SDK** are documented in this file.
- GCP `--skip-api-check` command line flag [(#8575)](https://github.com/prowler-cloud/prowler/pull/8575)
### Changed
- Refine kisa isms-p compliance mapping [(#8479)](https://github.com/prowler-cloud/prowler/pull/8479)
- Improve AWS Security Hub region check using multiple threads [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365)
### Fixed
- Resource metadata error in `s3_bucket_shadow_resource_vulnerability` check [(#8572)](https://github.com/prowler-cloud/prowler/pull/8572)
- GitHub App authentication through API fails with auth_method validation error [(#8587)](https://github.com/prowler-cloud/prowler/pull/8587)
- AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533)
@@ -383,6 +431,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.10.2] (Prowler v5.10.2)
### Fixed
- Order requirements by ID in Prowler ThreatScore AWS compliance framework [(#8495)](https://github.com/prowler-cloud/prowler/pull/8495)
- Add explicit resource name to GCP and Azure Defender checks [(#8352)](https://github.com/prowler-cloud/prowler/pull/8352)
- Validation errors in Azure and M365 providers [(#8353)](https://github.com/prowler-cloud/prowler/pull/8353)
@@ -397,6 +446,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.10.1] (Prowler v5.10.1)
### Fixed
- Remove invalid requirements from CIS 1.0 for GitHub provider [(#8472)](https://github.com/prowler-cloud/prowler/pull/8472)
---
@@ -404,6 +454,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.10.0] (Prowler v5.10.0)
### Added
- `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321)
- `bedrock_api_key_no_long_term_credentials` check for AWS provider [(#8396)](https://github.com/prowler-cloud/prowler/pull/8396)
- Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271)
@@ -416,11 +467,13 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Use `trivy` as engine for IaC provider [(#8466)](https://github.com/prowler-cloud/prowler/pull/8466)
### Changed
- Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347)
- Revert import of `checkov` python library [(#8385)](https://github.com/prowler-cloud/prowler/pull/8385)
- Updated policy mapping in ISMS-P compliance file for improved alignment [(#8367)](https://github.com/prowler-cloud/prowler/pull/8367)
### Fixed
- False positives in SQS encryption check for ephemeral queues [(#8330)](https://github.com/prowler-cloud/prowler/pull/8330)
- Add protocol validation check in security group checks to ensure proper protocol matching [(#8374)](https://github.com/prowler-cloud/prowler/pull/8374)
- Add missing audit evidence for controls 1.1.4 and 2.5.5 for ISMS-P compliance. [(#8386)](https://github.com/prowler-cloud/prowler/pull/8386)
@@ -444,6 +497,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.9.2] (Prowler v5.9.2)
### Fixed
- Use the correct resource name in `defender_domain_dkim_enabled` check [(#8334)](https://github.com/prowler-cloud/prowler/pull/8334)
---
@@ -451,6 +505,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.9.0] (Prowler v5.9.0)
### Added
- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123)
- `storage_smb_protocol_version_is_latest` check for Azure provider [(#8128)](https://github.com/prowler-cloud/prowler/pull/8128)
- `vm_backup_enabled` check for Azure provider [(#8182)](https://github.com/prowler-cloud/prowler/pull/8182)
@@ -463,9 +518,11 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add `test_connection` method to GitHub provider [(#8248)](https://github.com/prowler-cloud/prowler/pull/8248)
### Changed
- Refactor the Azure Defender get security contact configuration method to use the API REST endpoint instead of the SDK [(#8241)](https://github.com/prowler-cloud/prowler/pull/8241)
### Fixed
- Title & description wording for `iam_user_accesskey_unused` check for AWS provider [(#8233)](https://github.com/prowler-cloud/prowler/pull/8233)
- Add GitHub provider to lateral panel in documentation and change -h environment variable output [(#8246)](https://github.com/prowler-cloud/prowler/pull/8246)
- Show `m365_identity_type` and `m365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
@@ -485,6 +542,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.8.1] (Prowler v5.8.1)
### Fixed
- Detect wildcarded ARNs in sts:AssumeRole policy resources [(#8164)](https://github.com/prowler-cloud/prowler/pull/8164)
- List all streams and `firehose_stream_encrypted_at_rest` logic [(#8213)](https://github.com/prowler-cloud/prowler/pull/8213)
- Allow empty values for http_endpoint in templates [(#8184)](https://github.com/prowler-cloud/prowler/pull/8184)
@@ -537,6 +595,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- New check `codebuild_project_not_publicly_accessible` for AWS provider [(#8127)](https://github.com/prowler-cloud/prowler/pull/8127)
### Fixed
- Consolidate Azure Storage file service properties to the account level, improving the accuracy of the `storage_ensure_file_shares_soft_delete_is_enabled` check [(#8087)](https://github.com/prowler-cloud/prowler/pull/8087)
- Migrate Azure VM service and managed disk logic to Pydantic models for better serialization and type safety, and update all related tests to use the new models and fix UUID handling [(#https://github.com/prowler-cloud/prowler/pull/8151)](https://github.com/prowler-cloud/prowler/pull/https://github.com/prowler-cloud/prowler/pull/8151)
- `organizations_scp_check_deny_regions` check to pass when SCP policies have no statements [(#8091)](https://github.com/prowler-cloud/prowler/pull/8091)
@@ -547,9 +606,11 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Handle empty name in Azure Defender and GCP checks [(#8120)](https://github.com/prowler-cloud/prowler/pull/8120)
### Changed
- Reworked `S3.test_connection` to match the AwsProvider logic [(#8088)](https://github.com/prowler-cloud/prowler/pull/8088)
### Removed
- OCSF version number references to point always to the latest [(#8064)](https://github.com/prowler-cloud/prowler/pull/8064)
---
@@ -557,6 +618,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.7.5] (Prowler v5.7.5)
### Fixed
- Use unified timestamp for all requirements [(#8059)](https://github.com/prowler-cloud/prowler/pull/8059)
- Add EKS to service without subservices [(#7959)](https://github.com/prowler-cloud/prowler/pull/7959)
- `apiserver_strong_ciphers_only` check for K8S provider [(#7952)](https://github.com/prowler-cloud/prowler/pull/7952)
@@ -575,6 +637,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.7.3] (Prowler v5.7.3)
### Fixed
- Automatically encrypt password in Microsoft365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
- Remove last encrypted password appearances [(#7825)](https://github.com/prowler-cloud/prowler/pull/7825)
@@ -583,6 +646,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.7.2] (Prowler v5.7.2)
### Fixed
- `m365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
- `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
- `m365_powershell` to close the PowerShell sessions in msgraph services [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
@@ -595,6 +659,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.7.0] (Prowler v5.7.0)
### Added
- Update the compliance list supported for each provider from docs [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
- Allow setting cluster name in in-cluster mode in Kubernetes [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
- Prowler ThreatScore for M365 provider [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
@@ -613,6 +678,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- CIS 5.0 compliance framework for AWS [(7766)](https://github.com/prowler-cloud/prowler/pull/7766)
### Fixed
- Update CIS 4.0 for M365 provider [(#7699)](https://github.com/prowler-cloud/prowler/pull/7699)
- Update and upgrade CIS for all the providers [(#7738)](https://github.com/prowler-cloud/prowler/pull/7738)
- Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible` [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750)
@@ -623,6 +689,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.6.0] (Prowler v5.6.0)
### Added
- SOC2 compliance framework to Azure [(#7489)](https://github.com/prowler-cloud/prowler/pull/7489)
- Check for unused Service Accounts in GCP [(#7419)](https://github.com/prowler-cloud/prowler/pull/7419)
- Powershell to Microsoft365 [(#7331)](https://github.com/prowler-cloud/prowler/pull/7331)
@@ -672,6 +739,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681)
### Fixed
- Package name location in pyproject.toml while replicating for prowler-cloud [(#7531)](https://github.com/prowler-cloud/prowler/pull/7531)
- Remove cache in PyPI release action [(#7532)](https://github.com/prowler-cloud/prowler/pull/7532)
- The correct values for logger.info inside iam service [(#7526)](https://github.com/prowler-cloud/prowler/pull/7526)
@@ -692,6 +760,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.5.1] (Prowler v5.5.1)
### Fixed
- Default name to contacts in Azure Defender [(#7483)](https://github.com/prowler-cloud/prowler/pull/7483)
- Handle projects without ID in GCP [(#7496)](https://github.com/prowler-cloud/prowler/pull/7496)
- Restore packages location in PyProject [(#7510)](https://github.com/prowler-cloud/prowler/pull/7510)
+6 -2
View File
@@ -121,7 +121,9 @@
{
"Id": "1.2.2",
"Description": "Shared mailboxes are used when multiple people need access to the same mailbox, such as a company information or support email address, reception desk, or other function that might be shared by multiple people.Users with permissions to the group mailbox can send as or send on behalf of the mailbox email address if the administrator has given that user permissions to do that. This is particularly useful for help and support mailboxes because users can send emails from \"Contoso Support\" or \"Building A Reception Desk.\"Shared mailboxes are created with a corresponding user account using a system generated password that is unknown at the time of creation.The recommended state is `Sign in blocked` for `Shared mailboxes`.",
"Checks": [],
"Checks": [
"exchange_shared_mailbox_sign_in_disabled"
],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -691,7 +693,9 @@
{
"Id": "2.4.4",
"Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes malware and high confidence phishing. When ZAP for Teams protection blocks a message, the message is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery.",
"Checks": [],
"Checks": [
"defender_zap_for_teams_enabled"
],
"Attributes": [
{
"Section": "2 Microsoft 365 Defender",
+6 -2
View File
@@ -121,7 +121,9 @@
{
"Id": "1.2.2",
"Description": "Shared mailboxes are used when multiple people need access to the same mailbox, such as a company information or support email address, reception desk, or other function that might be shared by multiple people. Shared mailboxes are created with a corresponding user account using a system generated password that is unknown at the time of creation. The recommended state is Sign in blocked for Shared mailboxes.",
"Checks": [],
"Checks": [
"exchange_shared_mailbox_sign_in_disabled"
],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -753,7 +755,9 @@
{
"Id": "2.4.4",
"Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes malware and high confidence phishing. When ZAP for Teams protection blocks a message, the message is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery.",
"Checks": [],
"Checks": [
"defender_zap_for_teams_enabled"
],
"Attributes": [
{
"Section": "2 Microsoft 365 Defender",
+13 -11
View File
@@ -112,12 +112,13 @@
}
],
"Checks": [
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled",
"defender_antiphishing_policy_configured",
"defender_antispam_outbound_policy_configured",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_antiphishing_policy_configured",
"entra_admin_users_phishing_resistant_mfa_enabled"
"defender_zap_for_teams_enabled",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
]
},
{
@@ -344,15 +345,15 @@
}
],
"Checks": [
"defender_antispam_outbound_policy_configured",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_antispam_connection_filter_policy_empty_ip_allowlist",
"defender_antispam_connection_filter_policy_safe_list_off",
"defender_antispam_outbound_policy_configured",
"defender_antispam_outbound_policy_forwarding_disabled",
"defender_antispam_policy_inbound_no_allowed_domains"
"defender_antispam_policy_inbound_no_allowed_domains",
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_zap_for_teams_enabled"
]
},
{
@@ -368,11 +369,11 @@
}
],
"Checks": [
"defender_antispam_outbound_policy_configured",
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_antispam_outbound_policy_configured",
"defender_malware_policy_notifications_internal_users_malware_enabled"
"defender_zap_for_teams_enabled"
]
},
{
@@ -691,6 +692,7 @@
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_zap_for_teams_enabled",
"teams_external_domains_restricted",
"teams_external_users_cannot_start_conversations"
]
+1 -1
View File
@@ -38,7 +38,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.17.0"
prowler_version = "5.18.0"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
@@ -0,0 +1,40 @@
{
"Provider": "aws",
"CheckID": "codebuild_project_webhook_filters_use_anchored_patterns",
"CheckTitle": "CodeBuild project webhook filters use anchored regex patterns",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "codebuild",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsCodeBuildProject",
"ResourceGroup": "devops",
"Description": "AWS CodeBuild webhook filters using `ACTOR_ACCOUNT_ID`, `HEAD_REF`, or `BASE_REF` have regex patterns anchored with `^` (start) and `$` (end) to enforce exact matching and prevent substring bypass attacks.",
"Risk": "Unanchored patterns expose CI/CD pipelines to **CodeBreach** attacks. Attackers can bypass `ACTOR_ACCOUNT_ID` filters by creating GitHub accounts with IDs containing trusted values as substrings. **Confidentiality**: Credentials leaked via build logs. **Integrity**: Malicious code injected into builds. **Availability**: Resource exhaustion through unauthorized builds.",
"RelatedUrl": "",
"Remediation": {
"Code": {
"CLI": "aws codebuild update-webhook --project-name <PROJECT_NAME> --filter-groups '[[{\"type\":\"ACTOR_ACCOUNT_ID\",\"pattern\":\"^123456$|^234567$\"}]]'",
"NativeIaC": "AWSTemplateFormatVersion: '2010-09-09'\nResources:\n CodeBuildWebhook:\n Type: AWS::CodeBuild::Project\n Properties:\n Triggers:\n Webhook: true\n FilterGroups:\n - - Type: ACTOR_ACCOUNT_ID\n Pattern: '^123456$|^234567$' # Anchored pattern",
"Other": "1. Open AWS Console and navigate to CodeBuild. 2. Select the project with webhook filters. 3. Click Edit and go to Primary source webhook events. 4. For each filter using ACTOR_ACCOUNT_ID, HEAD_REF, or BASE_REF, update patterns to include ^ at start and $ at end (e.g., change '123456|234567' to '^123456$|^234567$'). 5. Save changes.",
"Terraform": "resource \"aws_codebuild_webhook\" \"example\" {\n project_name = aws_codebuild_project.example.name\n filter_group {\n filter {\n type = \"ACTOR_ACCOUNT_ID\"\n pattern = \"^123456$|^234567$\" # Anchored pattern\n }\n }\n}"
},
"Recommendation": {
"Text": "Anchor all webhook filter patterns with `^` (start) and `$` (end) to enforce exact matching. For multiple values use: `^value1$|^value2$`. This prevents attackers from bypassing filters using substring matches.",
"Url": "https://hub.prowler.com/check/codebuild_project_webhook_filters_use_anchored_patterns"
}
},
"Categories": [
"software-supply-chain",
"ci-cd"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check targets the CodeBreach vulnerability disclosed by Wiz Research. The vulnerability allows attackers to bypass ACTOR_ACCOUNT_ID filters by creating GitHub accounts with IDs that contain trusted IDs as substrings.",
"AdditionalURLs": [
"https://www.wiz.io/blog/wiz-research-codebreach-vulnerability-aws-codebuild",
"https://docs.aws.amazon.com/codebuild/latest/userguide/github-webhook.html"
]
}
@@ -0,0 +1,58 @@
from typing import List
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.codebuild.codebuild_client import codebuild_client
HIGH_RISK_FILTER_TYPES = {"ACTOR_ACCOUNT_ID", "HEAD_REF", "BASE_REF"}
def is_pattern_anchored(pattern: str) -> bool:
"""Check if each alternative in a pipe-separated pattern is anchored with ^ and $."""
if not pattern:
return True
for alt in pattern.split("|"):
alt = alt.strip()
if alt and not (alt.startswith("^") and alt.endswith("$")):
return False
return True
class codebuild_project_webhook_filters_use_anchored_patterns(Check):
def execute(self) -> List[Check_Report_AWS]:
findings = []
for project in codebuild_client.projects.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=project)
report.status = "PASS"
report.status_extended = (
f"CodeBuild project {project.name} has no webhook configured or all "
"webhook filter patterns are properly anchored."
)
if not project.webhook or not project.webhook.filter_groups:
findings.append(report)
continue
unanchored_filters = []
for filter_group in project.webhook.filter_groups:
for webhook_filter in filter_group.filters:
if webhook_filter.type in HIGH_RISK_FILTER_TYPES:
if not is_pattern_anchored(webhook_filter.pattern):
unanchored_filters.append(
f"{webhook_filter.type}: '{webhook_filter.pattern}'"
)
if unanchored_filters:
report.status = "FAIL"
filters_str = ", ".join(unanchored_filters[:3])
if len(unanchored_filters) > 3:
filters_str += f" and {len(unanchored_filters) - 3} more"
report.status_extended = (
f"CodeBuild project {project.name} has webhook filters with "
f"unanchored patterns that could allow bypass attacks: {filters_str}."
)
findings.append(report)
return findings
@@ -122,6 +122,29 @@ class Codebuild(AWSService):
project.tags = project_info.get("tags", [])
project.service_role_arn = project_info.get("serviceRole", "")
project.project_visibility = project_info.get("projectVisibility", "")
# Extract webhook configuration
webhook_data = project_info.get("webhook")
if webhook_data:
filter_groups = []
for fg in webhook_data.get("filterGroups", []):
filters = []
for f in fg:
filters.append(
WebhookFilter(
type=f.get("type", ""),
pattern=f.get("pattern", ""),
exclude_matched_pattern=f.get(
"excludeMatchedPattern", False
),
)
)
filter_groups.append(WebhookFilterGroup(filters=filters))
project.webhook = Webhook(
filter_groups=filter_groups,
branch_filter=webhook_data.get("branchFilter"),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -209,6 +232,27 @@ class CloudWatchLogs(BaseModel):
stream_name: str
class WebhookFilter(BaseModel):
"""Represents a single filter in a webhook filter group."""
type: str # ACTOR_ACCOUNT_ID, HEAD_REF, BASE_REF, EVENT, etc.
pattern: str
exclude_matched_pattern: bool = False
class WebhookFilterGroup(BaseModel):
"""Represents a group of filters (AND logic within group)."""
filters: List[WebhookFilter] = []
class Webhook(BaseModel):
"""Represents the webhook configuration for a CodeBuild project."""
filter_groups: List[WebhookFilterGroup] = []
branch_filter: Optional[str] = None
class Project(BaseModel):
name: str
arn: str
@@ -224,6 +268,7 @@ class Project(BaseModel):
cloudwatch_logs: Optional[CloudWatchLogs]
tags: Optional[list]
project_visibility: Optional[str] = None
webhook: Optional[Webhook] = None
class ExportConfig(BaseModel):
@@ -1,6 +1,10 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
MAX_WORKERS = 10
class AzureService:
def __init__(
@@ -20,6 +24,25 @@ class AzureService:
self.audit_config = provider.audit_config
self.fixer_config = provider.fixer_config
self.thread_pool = ThreadPoolExecutor(max_workers=MAX_WORKERS)
def __threading_call__(self, call, iterator):
"""Execute a function across multiple items using threading."""
items = list(iterator) if not isinstance(iterator, list) else iterator
futures = {self.thread_pool.submit(call, item): item for item in items}
results = []
for future in as_completed(futures):
try:
result = future.result()
if result is not None:
results.append(result)
except Exception:
pass
return results
def __set_clients__(self, identity, session, service, region_config):
clients = {}
try:
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "aisearch_service_not_publicly_accessible",
"CheckTitle": "Restrict public network access to the AI Search Service",
"CheckTitle": "AI Search service has public network access disabled",
"CheckType": [],
"ServiceName": "aisearch",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureSearchService",
"ResourceType": "microsoft.search/searchservices",
"ResourceGroup": "database",
"Description": "Ensure that public network access to the Search Service is restricted.",
"Risk": "Public accessibility exposes the Search Service to potential attacks, unauthorized usage, and data breaches. Restricting access minimizes the surface area for attacks and ensures that only authorized networks can access the search service.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal",
"Description": "**Azure AI Search service** limits its data-plane endpoint by disabling **public network access**. This evaluation checks whether the service only permits connections via **private endpoints** or narrowly scoped, explicitly allowed sources.",
"Risk": "Internet-reachable search endpoints impact CIA:\n- Confidentiality: unauthorized queries reveal indexed data/metadata\n- Integrity: stolen admin/query keys allow index changes or deletions\n- Availability: abuse and scanning drive throttling and outages",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/azure-cognitive-search-security-baseline",
"https://www.azadvertizer.net/azpolicyadvertizer/9cee519f-d9c1-4fd9-9f79-24ec3449ed30.html",
"https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal"
],
"Remediation": {
"Code": {
"CLI": "az search service update --resource-group <resource_group_name> --name <service_name> --public-access disabled",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az search service update --name <service_name> --resource-group <resource_group_name> --public-access disabled",
"NativeIaC": "```bicep\n// Disable public network access for an Azure AI Search service\nresource search 'Microsoft.Search/searchServices@2023-11-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n sku: { name: 'basic' }\n properties: {\n publicNetworkAccess: 'disabled' // CRITICAL: Disables public access so the service is not reachable from the internet\n }\n}\n```",
"Other": "1. In the Azure portal, open your AI Search service\n2. Go to Settings > Networking\n3. Under Public network access, select Disabled\n4. Click Save\n5. Wait a few minutes and re-run the check",
"Terraform": "```hcl\n# Disable public network access for Azure AI Search\nresource \"azurerm_search_service\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<location>\"\n sku = \"basic\"\n\n public_network_access_enabled = false # CRITICAL: Disables public access to pass the check\n}\n```"
},
"Recommendation": {
"Text": "Ensure that the necessary virtual network configurations or IP rules are in place to allow access from required services once public access is restricted. Review the network access settings regularly to maintain a secure environment. To restrict public network access to your Search Service: 1. Navigate to your Search Service y in the Azure Portal. 2. Under 'Settings'->'Networking', configure the 'Public network access' settings to 'Disabled'. 3. Set up virtual network service endpoints or private endpoints as needed for secure access. 4. Review and adjust IP access rules as necessary.",
"Url": "https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal"
"Text": "Set `Public network access: Disabled`. Prefer **Private Link** and restrict any residual exposure to specific sources only. Use **least privilege** with Microsoft Entra ID RBAC instead of keys. Apply **defense in depth** with IP rules/trusted services, enable logs, and review access lists regularly.",
"Url": "https://hub.prowler.com/check/aisearch_service_not_publicly_accessible"
}
},
"Categories": [
"internet-exposed",
"gen-ai"
],
"DependsOn": [],
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "aks_cluster_rbac_enabled",
"CheckTitle": "Ensure AKS RBAC is enabled",
"CheckTitle": "AKS cluster has RBAC enabled",
"CheckType": [],
"ServiceName": "aks",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.ContainerService/ManagedClusters",
"Severity": "high",
"ResourceType": "microsoft.containerservice/managedclusters",
"ResourceGroup": "container",
"Description": "Azure Kubernetes Service (AKS) can be configured to use Azure Active Directory (AD) for user authentication. In this configuration, you sign in to an AKS cluster using an Azure AD authentication token. You can also configure Kubernetes role-based access control (Kubernetes RBAC) to limit access to cluster resources based a user's identity or group membership.",
"Risk": "Kubernetes RBAC and AKS help you secure your cluster access and provide only the minimum required permissions to developers and operators.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/aks/azure-ad-rbac?tabs=portal",
"Description": "**AKS clusters** with **Kubernetes RBAC** enforce authorization through roles and bindings mapped to identities and groups.\n\nThis evaluates whether the cluster has RBAC enabled to control access to namespaces and cluster-wide resources.",
"Risk": "Without **Kubernetes RBAC**, authorization becomes overly broad, weakening **least privilege**. Compromised credentials could read secrets, alter workloads, or delete services, impacting **confidentiality**, **integrity**, and **availability**, and enabling lateral movement across the cluster.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/enable-role-based-access-control-for-kubernetes-service.html#",
"https://learn.microsoft.com/en-us/azure/aks/azure-ad-rbac?tabs=portal"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/enable-role-based-access-control-for-kubernetes-service.html#",
"Terraform": "https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_2#terraform"
"NativeIaC": "```bicep\n// Enable Kubernetes RBAC on AKS\nresource <example_resource_name> 'Microsoft.ContainerService/managedClusters@2024-05-01' = {\n name: '<example_resource_name>'\n location: '<example_location>'\n properties: {\n enableRBAC: true // Critical: turns on Kubernetes RBAC to pass the check\n }\n}\n```",
"Other": "1. In Azure portal, go to Kubernetes services > Create (or edit your deployment template)\n2. On the Authentication tab, set Kubernetes RBAC to Enabled\n3. Review + Create to deploy (re-create the cluster if the setting can't be changed on an existing one)",
"Terraform": "```hcl\n# AKS with Kubernetes RBAC enabled\nresource \"azurerm_kubernetes_cluster\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<example_location>\"\n resource_group_name = \"<example_resource_name>\"\n dns_prefix = \"<example_resource_name>\"\n\n default_node_pool {\n name = \"default\"\n node_count = 1\n vm_size = \"Standard_DS2_v2\"\n }\n\n identity {\n type = \"SystemAssigned\"\n }\n\n role_based_access_control_enabled = true # Critical: enables Kubernetes RBAC to pass the check\n}\n```"
},
"Recommendation": {
"Text": "",
"Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle"
"Text": "Enable **Kubernetes RBAC** and design permissions with **least privilege**: scope roles to namespaces, grant access via groups, apply deny-by-default, and separate duties for admins and operators.\n\nIntegrate with **Microsoft Entra ID** and review/audit role bindings to maintain **defense in depth**.",
"Url": "https://hub.prowler.com/check/aks_cluster_rbac_enabled"
}
},
"Categories": [],
"Categories": [
"cluster-security",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,39 @@
{
"Provider": "azure",
"CheckID": "aks_clusters_created_with_private_nodes",
"CheckTitle": "Ensure clusters are created with Private Nodes",
"CheckTitle": "AKS cluster nodes do not have public IP addresses",
"CheckType": [],
"ServiceName": "aks",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.ContainerService/ManagedClusters",
"ResourceType": "microsoft.containerservice/managedclusters",
"ResourceGroup": "container",
"Description": "Disable public IP addresses for cluster nodes, so that they only have private IP addresses. Private Nodes are nodes with no public IP addresses.",
"Risk": "Disabling public IP addresses on cluster nodes restricts access to only internal networks, forcing attackers to obtain local network access before attempting to compromise the underlying Kubernetes hosts.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/aks/private-clusters",
"Description": "**AKS agent pools** use only private addressing, with node public IP assignment disabled (`enableNodePublicIP=false`). Clusters where any pool assigns a public IP to nodes are identified.",
"Risk": "**Public node IPs** expose worker VMs to Internet scanning and exploit attempts against OS or kubelet services. Successful compromise can lead to stolen secrets and images (**confidentiality**), workload tampering (**integrity**), and node/resource exhaustion via DDoS or cryptomining (**availability**).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/private-cluster-nodes.html",
"https://learn.microsoft.com/en-us/azure/aks/access-private-cluster",
"https://learn.microsoft.com/en-us/azure/aks/private-clusters"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"NativeIaC": "```bicep\n// AKS cluster with nodes not assigned public IPs\nresource mc '<example_resource_name>@Microsoft.ContainerService/managedClusters@2024-05-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n identity: {\n type: 'SystemAssigned'\n }\n properties: {\n dnsPrefix: '<example_resource_name>'\n agentPoolProfiles: [\n {\n name: 'nodepool1'\n count: 1\n vmSize: 'Standard_DS2_v2'\n enableNodePublicIP: false // CRITICAL: ensures nodes have no public IPs\n }\n ]\n }\n}\n```",
"Other": "1. In the Azure portal, go to Kubernetes services > <your AKS cluster> > Node pools\n2. For any node pool showing Node public IP: Enabled, click Add node pool\n3. Create the new pool with the same size/count; set Node public IP to Disabled (or uncheck Enable node public IP)\n4. Wait until the new pool is Ready\n5. If replacing a system pool, set the new pool to Mode: System (or Set as default system pool)\n6. Delete the old node pool(s) that had Node public IP enabled\n7. Verify all node pools show Node public IP: Disabled",
"Terraform": "```hcl\n# AKS cluster with node public IPs disabled\nresource \"azurerm_kubernetes_cluster\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n dns_prefix = \"<example_resource_name>\"\n\n default_node_pool {\n name = \"default\"\n node_count = 1\n vm_size = \"Standard_DS2_v2\"\n enable_node_public_ip = false # CRITICAL: ensures nodes have no public IPs\n }\n\n identity {\n type = \"SystemAssigned\"\n }\n}\n```"
},
"Recommendation": {
"Text": "",
"Url": "https://learn.microsoft.com/en-us/azure/aks/access-private-cluster"
"Text": "Disable **public node IPs** on all agent pools (`enableNodePublicIP=false`) and route egress via a **NAT gateway** or **Azure Firewall**. Apply **least privilege** with NSGs blocking Internet ingress, use **private endpoints** and **bastion/VPN** for admin access, and adopt **defense in depth** with continuous hardening and monitoring.",
"Url": "https://hub.prowler.com/check/aks_clusters_created_with_private_nodes"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries",
"cluster-security"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,40 @@
{
"Provider": "azure",
"CheckID": "aks_clusters_public_access_disabled",
"CheckTitle": "Ensure clusters are created with Private Endpoint Enabled and Public Access Disabled",
"CheckTitle": "AKS cluster has a private endpoint and node public access is disabled",
"CheckType": [],
"ServiceName": "aks",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.ContainerService/ManagedClusters",
"ResourceType": "microsoft.containerservice/managedclusters",
"ResourceGroup": "container",
"Description": "Disable access to the Kubernetes API from outside the node network if it is not required.",
"Risk": "In a private cluster, the master node has two endpoints, a private and public endpoint. The private endpoint is the internal IP address of the master, behind an internal load balancer in the master's wirtual network. Nodes communicate with the master using the private endpoint. The public endpoint enables the Kubernetes API to be accessed from outside the master's virtual network. Although Kubernetes API requires an authorized token to perform sensitive actions, a vulnerability could potentially expose the Kubernetes publically with unrestricted access. Additionally, an attacker may be able to identify the current cluster and Kubernetes API version and determine whether it is vulnerable to an attack. Unless required, disabling public endpoint will help prevent such threats, and require the attacker to be on the master's virtual network to perform any attack on the Kubernetes API.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/aks/private-clusters?tabs=azure-portal",
"Description": "**AKS clusters** expose a **private control plane FQDN** and agent pools have `enable_node_public_ip=false`.\n\nThe evaluation focuses on the presence of a private FQDN and the absence of public IPs on nodes.",
"Risk": "Exposed node IPs or a publicly reachable API increase attack surface, impacting **confidentiality** and **integrity**.\n- Internet scans reach kubelet, NodePort, or management agents\n- Exploits enable RCE and **lateral movement**\n- Metadata/secret theft leads to **credential compromise**\n\n**Availability** is at risk from DDoS on exposed endpoints.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/aks/private-clusters?tabs=azure-portal",
"https://learn.microsoft.com/en-us/azure/aks/access-private-cluster?tabs=azure-cli",
"https://support.icompaas.com/support/solutions/articles/62000234657-ensure-clusters-are-created-with-private-endpoint-enabled-and-public-access-disabled",
"https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-cis-aks17-542"
],
"Remediation": {
"Code": {
"CLI": "az aks update -n <cluster_name> -g <resource_group> --disable-public-fqdn",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "",
"NativeIaC": "```bicep\n// AKS cluster with private endpoint and nodes without public IPs\nresource aks 'Microsoft.ContainerService/managedClusters@2023-11-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n identity: {\n type: 'SystemAssigned'\n }\n properties: {\n dnsPrefix: 'dns'\n apiServerAccessProfile: {\n enablePrivateCluster: true // Critical: enables private cluster (private endpoint/FQDN)\n }\n agentPoolProfiles: [\n {\n name: 'nodepool1'\n count: 1\n vmSize: 'Standard_DS2_v2'\n enableNodePublicIP: false // Critical: disables public IPs on nodes\n }\n ]\n }\n}\n```",
"Other": "1. In the Azure portal, go to Azure Kubernetes Service and select your cluster\n2. Go to Networking > API server access\n3. Set Private cluster to Enabled and click Save\n4. Go to Node pools, open each pool, select Networking, set Public IP per node to Disabled, and Save\n5. Repeat step 4 for all node pools",
"Terraform": "```hcl\n# AKS cluster with private endpoint and nodes without public IPs\nresource \"azurerm_kubernetes_cluster\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n dns_prefix = \"dns\"\n\n private_cluster_enabled = true # Critical: enables private cluster (private endpoint/FQDN)\n\n default_node_pool {\n name = \"nodepool1\"\n node_count = 1\n vm_size = \"Standard_DS2_v2\"\n enable_node_public_ip = false # Critical: disables public IPs on nodes\n }\n\n identity {\n type = \"SystemAssigned\"\n }\n}\n```"
},
"Recommendation": {
"Text": "To use a private endpoint, create a new private endpoint in your virtual network then create a link between your virtual network and a new private DNS zone",
"Url": "https://learn.microsoft.com/en-us/azure/aks/access-private-cluster?tabs=azure-cli"
"Text": "Adopt **private clusters** and eliminate node public IPs:\n- Set `enable_node_public_ip=false` on all pools\n- Use **Private Link** or peered VNets with VPN/ExpressRoute for admin access\n- If public API is unavoidable, restrict with IP ranges and strong auth\n- Enforce NSGs/firewalls and **least privilege** for layered defense",
"Url": "https://hub.prowler.com/check/aks_clusters_public_access_disabled"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries",
"cluster-security"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "aks_network_policy_enabled",
"CheckTitle": "Ensure Network Policy is Enabled and set as appropriate",
"CheckTitle": "AKS cluster has network policy enabled",
"CheckType": [],
"ServiceName": "aks",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.ContainerService/managedClusters",
"ResourceType": "microsoft.containerservice/managedclusters",
"ResourceGroup": "container",
"Description": "When you run modern, microservices-based applications in Kubernetes, you often want to control which components can communicate with each other. The principle of least privilege should be applied to how traffic can flow between pods in an Azure Kubernetes Service (AKS) cluster. Let's say you likely want to block traffic directly to back-end applications. The Network Policy feature in Kubernetes lets you define rules for ingress and egress traffic between pods in a cluster.",
"Risk": "All pods in an AKS cluster can send and receive traffic without limitations, by default. To improve security, you can define rules that control the flow of traffic. Back-end applications are often only exposed to required front-end services, for example. Or, database components are only accessible to the application tiers that connect to them. Network Policy is a Kubernetes specification that defines access policies for communication between Pods. Using Network Policies, you define an ordered set of rules to send and receive traffic and apply them to a collection of pods that match one or more label selectors. These network policy rules are defined as YAML manifests. Network policies can be included as part of a wider manifest that also creates a deployment or service.",
"RelatedUrl": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-network-security#ns-2-connect-private-networks-together",
"Description": "**AKS clusters** enforce **Kubernetes network policies** so that pod-to-pod traffic is governed by explicit ingress and egress rules. The finding evaluates whether a cluster has network policy enforcement enabled to support fine-grained, label-based segmentation between workloads.",
"Risk": "Without network policy, pods can talk to any pod:\n- Easy lateral movement after a pod compromise\n- Unrestricted access to backend services and data\n- Covert exfiltration/C2 via East-West traffic\n\nThis harms **confidentiality** and **integrity** and amplifies the blast radius of runtime exploits.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/aks/use-network-policies",
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-network-security#ns-2-connect-private-networks-together",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/enable-network-policy-support.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_4#terraform"
"CLI": "az aks update --resource-group <RESOURCE_GROUP> --name <CLUSTER_NAME> --network-policy calico",
"NativeIaC": "```bicep\n// Enable network policy on AKS\nparam location string = resourceGroup().location\n\nresource aks 'Microsoft.ContainerService/managedClusters@2024-05-01' = {\n name: '<example_resource_name>'\n location: location\n dnsPrefix: '<example_dns_prefix>'\n identity: { type: 'SystemAssigned' }\n agentPoolProfiles: [\n {\n name: 'systempool'\n vmSize: 'Standard_DS2_v2'\n count: 1\n }\n ]\n networkProfile: {\n networkPlugin: 'azure'\n networkPolicy: 'calico' // Critical: enables AKS network policy enforcement\n }\n}\n```",
"Other": "1. In Azure Portal, go to Kubernetes services and select your cluster\n2. Open Networking (or Settings > Networking)\n3. Set Network policy to Azure or Calico\n4. Click Save to apply",
"Terraform": "```hcl\n# Enable network policy on AKS\nresource \"azurerm_kubernetes_cluster\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<LOCATION>\"\n resource_group_name = \"<RESOURCE_GROUP>\"\n dns_prefix = \"<example_dns_prefix>\"\n\n default_node_pool {\n name = \"system\"\n node_count = 1\n vm_size = \"Standard_DS2_v2\"\n }\n\n identity {\n type = \"SystemAssigned\"\n }\n\n network_profile {\n network_plugin = \"azure\"\n network_policy = \"calico\" # Critical: enables AKS network policy\n }\n}\n```"
},
"Recommendation": {
"Text": "",
"Url": "https://learn.microsoft.com/en-us/azure/aks/use-network-policies"
"Text": "Enable **network policy enforcement** and apply **least privilege** segmentation.\n- Start with a `deny-all` baseline, allow only required flows\n- Define both ingress and egress policies\n- Use consistent labels/namespaces\n- Layer with **defense in depth** (RBAC, node isolation, private networking) for zero-trust East-West control.",
"Url": "https://hub.prowler.com/check/aks_network_policy_enabled"
}
},
"Categories": [],
"Categories": [
"trust-boundaries",
"cluster-security"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Network Policy requires the Network Policy add-on. This add-on is included automatically when a cluster with Network Policy is created, but for an existing cluster, needs to be added prior to enabling Network Policy. Enabling/Disabling Network Policy causes a rolling update of all cluster nodes, similar to performing a cluster upgrade. This operation is long-running and will block other operations on the cluster (including delete) until it has run to completion. If Network Policy is used, a cluster must have at least 2 nodes of type n1-standard-1 or higher. The recommended minimum size cluster to run Network Policy enforcement is 3 n1-standard-1 instances. Enabling Network Policy enforcement consumes additional resources in nodes. Specifically, it increases the memory footprint of the kube-system process by approximately 128MB, and requires approximately 300 millicores of CPU."
@@ -1,33 +1,36 @@
{
"Provider": "azure",
"CheckID": "apim_threat_detection_llm_jacking",
"CheckTitle": "Ensure Azure API Management is protected against LLM Jacking attacks",
"CheckTitle": "No potential LLM Jacking attacks detected across all Azure API Management instances",
"CheckType": [],
"ServiceName": "apim",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Azure API Management Instance",
"Severity": "critical",
"ResourceType": "microsoft.apimanagement/service",
"ResourceGroup": "api_gateway",
"Description": "This check analyzes Azure API Management diagnostic logs in Log Analytics to detect potential LLM Jacking attacks by monitoring the frequency of LLM-related operations (ImageGenerations_Create, ChatCompletions_Create, Completions_Create) from individual IP addresses within a configurable time window.",
"Risk": "LLM Jacking attacks can lead to unauthorized access to AI models, potential data exfiltration, increased costs, and abuse of AI services. Attackers may use these endpoints to generate content, bypass rate limits, or access premium AI capabilities without proper authorization.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/api-management/monitor-api-management",
"Description": "**API Management** diagnostic logs in Log Analytics are analyzed for **LLM-related operations**. Requests are grouped by caller IP, the number of distinct monitored actions (e.g., `ChatCompletions_Create`, `ImageGenerations_Create`) within a configurable `minutes` window is measured, and that ratio is compared to a `threshold` to surface anomalous multi-action patterns.",
"Risk": "Concentrated LLM activity from one IP indicates **automation or leaked credentials**.\n- **Availability/cost**: rapid token burn and quota exhaustion\n- **Confidentiality**: exposure of prompts/completions and model details\n- **Integrity**: abuse of deployment/model actions enabling unauthorized changes or mass content generation",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/api-management/monitor-api-management"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"NativeIaC": "```bicep\n// Blocks a specific IP at the global (service) policy level for APIM\nparam apimName string\nparam blockedIp string\n\nresource apim 'Microsoft.ApiManagement/service@2023-05-01-preview' existing = {\n name: apimName\n}\n\nresource apimPolicy 'Microsoft.ApiManagement/service/policies@2023-05-01-preview' = {\n parent: apim\n name: 'policy'\n properties: {\n value: '<policies><inbound><base /><choose><!-- Critical: Block offending IP to stop LLM jacking -->\n <when condition=\"@(context.Request.IpAddress == \"${blockedIp}\")\">\n <return-response><set-status code=\"403\" reason=\"Forbidden\" /></return-response>\n </when>\n </choose></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>' // Critical: Policy XML that blocks the offending IP\n format: 'xml' // Critical: Apply policy as XML\n }\n}\n```",
"Other": "1. In the Azure portal, open your API Management instance\n2. Go to APIs > All APIs\n3. Click Policies (Inbound processing)\n4. Add a when block to block the offending IP:\n - Condition: @(context.Request.IpAddress == \"<OFFENDING_IP>\")\n - Action: return-response with status 403 Forbidden\n5. Save the policy\n6. Re-run the scan after the detection window elapses to confirm PASS",
"Terraform": "```hcl\n# Global APIM policy that blocks a specific IP\nresource \"azurerm_api_management_policy\" \"<example_resource_name>\" {\n api_management_id = \"<example_resource_id>\"\n\n # Critical: Policy XML that blocks the offending IP by returning 403\n xml_content = <<XML\n<policies>\n <inbound>\n <base />\n <choose>\n <!-- Critical: Block offending IP to stop LLM jacking -->\n <when condition=\"@(context.Request.IpAddress == \\\"<OFFENDING_IP>\\\")\">\n <return-response>\n <set-status code=\"403\" reason=\"Forbidden\" />\n </return-response>\n </when>\n </choose>\n </inbound>\n <backend><base /></backend>\n <outbound><base /></outbound>\n <on-error><base /></on-error>\n</policies>\nXML\n}\n```"
},
"Recommendation": {
"Text": "To protect against LLM Jacking attacks: 1. Enable diagnostic logging for APIM instances and send logs to Log Analytics workspace 2. Configure appropriate thresholds for LLM operation frequency monitoring 3. Set up alerts for suspicious activity patterns 4. Implement rate limiting and IP allowlisting for sensitive AI endpoints 5. Regularly review and analyze APIM access logs for anomalies",
"Url": "https://learn.microsoft.com/en-us/azure/api-management/monitor-api-management"
"Text": "Adopt **defense in depth** for LLM APIs:\n- Enforce **least privilege**; isolate management from inference\n- Prefer **managed identities** over keys; rotate secrets\n- Apply **quotas**, rate limiting, and IP allowlisting; use private access\n- Alert on anomalous action diversity; review logs\n\n*Tune `threshold` and `minutes` for your environment.*",
"Url": "https://hub.prowler.com/check/apim_threat_detection_llm_jacking"
}
},
"Categories": [
"threat-detection",
"monitoring",
"logging"
"gen-ai",
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "app_client_certificates_on",
"CheckTitle": "Ensure the web app has 'Client Certificates (Incoming client certificates)' set to 'On'",
"CheckTitle": "Web app requires incoming client certificates",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites/config",
"ResourceType": "microsoft.web/sites/config",
"ResourceGroup": "serverless",
"Description": "Client certificates allow for the app to request a certificate for incoming requests. Only clients that have a valid certificate will be able to reach the app.",
"Risk": "The TLS mutual authentication technique in enterprise environments ensures the authenticity of clients to the server. If incoming client certificates are enabled, then only an authenticated client who has valid certificates can access the app.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth?tabs=azurecli",
"Description": "**Azure App Service apps** enforce **mutual TLS** when `client certificate mode` is set to `Required`, meaning every inbound request must present a valid client certificate that the app can validate.",
"Risk": "Without **mTLS**, clients aren't cryptographically authenticated at the transport layer. Adversaries can reach endpoints using spoofed sources or stolen tokens, leading to unauthorized data access (confidentiality), request tampering (integrity), and automated abuse that degrades service (availability).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-identity-management#im-4-authenticate-server-and-services",
"https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth"
],
"Remediation": {
"Code": {
"CLI": "az webapp update --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --set clientCertEnabled=true",
"NativeIaC": "",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/azure/azure-networking-policies/bc_azr_networking_7#terraform"
"CLI": "az webapp update --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --set clientCertEnabled=true clientCertMode=Required",
"NativeIaC": "```bicep\n// Require client certificates for the web app\nresource appService 'Microsoft.Web/sites@2022-03-01' = {\n name: '<example_resource_name>'\n location: '<example_location>'\n properties: {\n serverFarmId: '<example_resource_id>'\n clientCertEnabled: true // Critical: enables mutual TLS\n clientCertMode: 'Required' // Critical: enforces client certs (passes the check)\n }\n}\n```",
"Other": "1. Open Azure Portal and go to App Services\n2. Select your web app\n3. Go to Configuration > General settings\n4. Under Client certificate mode, select Required\n5. Click Save",
"Terraform": "```hcl\n# Require client certificates for the App Service (use azurerm_linux_web_app or azurerm_windows_web_app)\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<example_location>\"\n resource_group_name = \"<example_resource_name>\"\n service_plan_id = \"<example_resource_id>\"\n\n client_certificate_enabled = true # Critical: enables mutual TLS\n client_certificate_mode = \"Required\" # Critical: enforces client certs (passes the check)\n\n site_config {}\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under the Settings section, Click on Configuration, then General settings 5. Set the option Client certificate mode located under Incoming client certificates to Require",
"Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-identity-management#im-4-authenticate-server-and-services"
"Text": "Set `client certificate mode` to `Required` and validate client certs in application logic (issuer, validity, revocation).\n\nEnforce HTTPS only, avoid broad exclusion paths, and manage certs via a trusted CA with rotation and revocation. Apply **least privilege** and **zero trust**, layering with private access or IP restrictions *as needed*.",
"Url": "https://hub.prowler.com/check/app_client_certificates_on"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Utilizing and maintaining client certificates will require additional work to obtain and manage replacement and key rotation."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_ensure_auth_is_set_up",
"CheckTitle": "Ensure App Service Authentication is set up for apps in Azure App Service",
"CheckTitle": "App Service app has App Service Authentication enabled",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Azure App Service Authentication is a feature that can prevent anonymous HTTP requests from reaching a Web Application or authenticate those with tokens before they reach the app. If an anonymous request is received from a browser, App Service will redirect to a logon page. To handle the logon process, a choice from a set of identity providers can be made, or a custom authentication mechanism can be implemented.",
"Risk": "By Enabling App Service Authentication, every incoming HTTP request passes through it before being handled by the application code. It also handles authentication of users with the specified provider (Azure Active Directory, Facebook, Google, Microsoft Account, and Twitter), validation, storing and refreshing of tokens, managing the authenticated sessions and injecting identity information into request headers.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization",
"Description": "**Azure App Service** can enforce built-in **Authentication/Authorization** (Easy Auth) so requests are authenticated by a provider before reaching app code.\n\nThis evaluates whether platform auth is enabled for the app and an identity provider is configured.",
"Risk": "Without platform **authentication**, apps may accept **anonymous requests**, enabling unauthorized access to APIs and data. Attackers can enumerate endpoints and bypass weak app checks, risking data exposure (C), unauthorized changes (I), and automated abuse impacting availability (A).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-app-service-authentication.html",
"https://learn.microsoft.com/en-us/azure/app-service/scenario-secure-app-authentication-app-service",
"https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization"
],
"Remediation": {
"Code": {
"CLI": "az webapp auth update --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --enabled true",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-app-service-authentication.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/bc_azr_general_2#terraform"
"NativeIaC": "```bicep\n// Enable App Service Authentication (Easy Auth) for an existing Web App\nresource auth 'Microsoft.Web/sites/config@2022-03-01' = {\n name: '<example_resource_name>/authsettingsV2'\n properties: {\n platformEnabled: true // CRITICAL: Turns on built-in authentication for the app\n }\n}\n```",
"Other": "1. Sign in to the Azure Portal and go to App Services\n2. Select <APP_NAME> and open Authentication\n3. Click Add identity provider, choose Microsoft, and click Add\n4. Save changes\n\nThis enables App Service Authentication for the app",
"Terraform": "```hcl\n# Enable App Service Authentication for an App Service (use azurerm_linux_web_app or azurerm_windows_web_app)\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_resource_name>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {}\n\n auth_settings_v2 {\n auth_enabled = true # CRITICAL: Enables built-in authentication (Easy Auth)\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under Setting section, click on Authentication 5. If no identity providers are set up, then click Add identity provider 6. Choose other parameters as per your requirements and click on Add",
"Url": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#website-contributor"
"Text": "Enable App Service **Authentication/Authorization** and set `Require authentication` for unauthenticated requests. Use **Microsoft Entra** or a trusted IdP, restrict tenants/audiences, enforce HTTPS, and apply **least privilege** with role/claim checks and Conditional Access for defense-in-depth. Avoid `Allow anonymous requests`.",
"Url": "https://hub.prowler.com/check/app_ensure_auth_is_set_up"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This is only required for App Services which require authentication. Enabling on site like a marketing or support website will prevent unauthenticated access which would be undesirable. Adding Authentication requirement will increase cost of App Service and require additional security components to facilitate the authentication"
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_ensure_http_is_redirected_to_https",
"CheckTitle": "Ensure Web App Redirects All HTTP traffic to HTTPS in Azure App Service",
"CheckTitle": "App Service web app redirects HTTP to HTTPS",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites/config",
"Severity": "high",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Azure Web Apps allows sites to run under both HTTP and HTTPS by default. Web apps can be accessed by anyone using non-secure HTTP links by default. Non-secure HTTP requests can be restricted and all HTTP requests redirected to the secure HTTPS port. It is recommended to enforce HTTPS-only traffic.",
"Risk": "Enabling HTTPS-only traffic will redirect all non-secure HTTP requests to HTTPS ports. HTTPS uses the TLS/SSL protocol to provide a secure connection which is both encrypted and authenticated. It is therefore important to support HTTPS for the security benefits.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-https",
"Description": "**Azure App Service web apps** redirect `HTTP` traffic to `HTTPS` when the `HTTPS Only` setting is enabled. This evaluation identifies apps that do not force secure transport by checking whether plaintext requests are automatically redirected to encrypted endpoints.",
"Risk": "Leaving **HTTP accessible** enables **man-in-the-middle** interception, credential and cookie theft, and response tampering. This undermines **confidentiality** and **integrity**, and can lead to session hijacking or downgrade attacks that bypass TLS.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-https",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-https-only-traffic.html#",
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-3-encrypt-sensitive-data-in-transit"
],
"Remediation": {
"Code": {
"CLI": "az webapp update --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --set httpsOnly=true",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-https-only-traffic.html#",
"Terraform": "https://docs.prowler.com/checks/azure/azure-networking-policies/bc_azr_networking_5#terraform"
"NativeIaC": "```bicep\n// Enable HTTPS-only redirect on an existing App Service\nresource app 'Microsoft.Web/sites@2022-09-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n httpsOnly: true // Critical: forces redirect from HTTP to HTTPS\n }\n}\n```",
"Other": "1. Sign in to the Azure portal and go to App Services\n2. Select your web app\n3. Go to TLS/SSL settings and set HTTPS Only to On\n4. Click Save",
"Terraform": "```hcl\n# Enforce HTTPS-only on an App Service\nresource \"azurerm_windows_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n service_plan_id = \"<example_resource_id>\"\n\n https_only = true # Critical: redirects HTTP to HTTPS\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under Setting section, Click on Configuration 5. In the General Settings section, set the HTTPS Only to On 6. Click Save",
"Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-3-encrypt-sensitive-data-in-transit"
"Text": "Enforce **HTTPS-only** for all apps.\n- Use trusted certificates and require `TLS 1.2` or later\n- Enable **HSTS** to prevent downgrade/mixed-content\n- Redirect legacy `http` links to `https`\n- Minimize HTTP exposure via WAF/CDN or private access\nApply **defense in depth** to protect data in transit.",
"Url": "https://hub.prowler.com/check/app_ensure_http_is_redirected_to_https"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "When it is enabled, every incoming HTTP request is redirected to the HTTPS port. This means an extra level of security will be added to the HTTP requests made to the app."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_ensure_java_version_is_latest",
"CheckTitle": "Ensure that 'Java version' is the latest, if used to run the Web App",
"CheckTitle": "App Service web app uses the latest supported Java version or 17 by default",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Periodically, newer versions are released for Java software either due to security flaws or to include additional functionality. Using the latest Java version for web apps is recommended in order to take advantage of security fixes, if any, and/or new functionalities of the newer version.",
"Risk": "Newer versions may contain security enhancements and additional functionality. Using the latest software version is recommended in order to take advantage of enhancements and new capabilities. With each software installation, organizations need to determine if a given update meets their requirements. They must also verify the compatibility and support provided for any additional software against the update revision that is selected.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings",
"Description": "**Azure App Service web apps** that run **Java** are assessed to ensure their configured runtime uses the **latest supported major version** (LTS) for the environment, across Linux and Windows.\n\n*Only apps with Java enabled are considered.*",
"Risk": "Using an **outdated Java runtime** enables known exploits like **remote code execution**, unsafe **deserialization**, and **cryptographic flaws**, risking data theft and tampering (**confidentiality, integrity**) and outages or takeover (**availability**). Unsupported versions also delay critical security patches.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-java.html",
"https://learn.microsoft.com/en-us/azure/app-service/configure-language-java?pivots=platform-linux#choosing-a-java-runtime-version",
"https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-java.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-java-version-is-the-latest-if-used-to-run-the-web-app#terraform"
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP> --name <APP_NAME> --linux-fx-version \"JAVA|17-java17\"",
"NativeIaC": "```bicep\n// Set Java 17 for a Linux App Service\nresource app 'Microsoft.Web/sites@2022-09-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource appConfig 'Microsoft.Web/sites/config@2022-09-01' = {\n name: '${app.name}/web'\n properties: {\n linuxFxVersion: 'JAVA|17-java17' // Critical: ensures runtime includes 'java17' so the check passes\n }\n}\n```",
"Other": "1. In the Azure portal, go to App Services and open your web app\n2. Select Settings > Configuration > General settings\n3. For Linux apps: under Stack settings, choose Java and set Java version to 17 (or choose Tomcat/JBoss with Java version 17)\n4. For Windows apps: under Stack settings, set Java version to 17\n5. Click Save and restart if prompted",
"Terraform": "```hcl\n# Linux Web App configured to use Java 17\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_resource_name>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {\n application_stack {\n java_version = \"17\" # Critical: sets Java to 17 to pass the check\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under Settings section, click on Configuration 5. Click on the General settings pane and ensure that for a Stack of Java the Major Version and Minor Version reflect the latest stable and supported release, and that the Java web server version is set to the auto-update option. NOTE: No action is required if Java version is set to Off, as Java is not used by your web app.",
"Url": "https://learn.microsoft.com/en-us/azure/app-service/configure-language-java?pivots=platform-linux#choosing-a-java-runtime-version"
"Text": "Adopt the **latest supported LTS Java** (`java <latest LTS>`) and standardize on that major line.\n- Enable automatic minor/patch updates\n- Validate upgrades in a staging environment before production\n- Retire deprecated runtimes and track vendor EOL\n\nApply **change management** and **defense in depth** to reduce exposure.",
"Url": "https://hub.prowler.com/check/app_ensure_java_version_is_latest"
}
},
"Categories": [],
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "If your app is written using version-dependent features or libraries, they may not be available on the latest version. If you wish to upgrade, research the impact thoroughly. Upgrading may have unforeseen consequences that could result in downtime."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_ensure_php_version_is_latest",
"CheckTitle": "Ensure That 'PHP version' is the Latest, If Used to Run the Web App",
"CheckTitle": "App Service web app uses the latest supported PHP version or 8.2 by default",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Periodically newer versions are released for PHP software either due to security flaws or to include additional functionality. Using the latest PHP version for web apps is recommended in order to take advantage of security fixes, if any, and/or additional functionalities of the newer version.",
"Risk": "Newer versions may contain security enhancements and additional functionality. Using the latest software version is recommended in order to take advantage of enhancements and new capabilities. With each software installation, organizations need to determine if a given update meets their requirements. They must also verify the compatibility and support provided for any additional software against the update revision that is selected.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings",
"Description": "**Azure App Service web apps** running **PHP** are evaluated to ensure the runtime is configured to the **latest supported** release. The finding compares the app's PHP stack (from `linuxFxVersion` or `php_version`) with the newest available version.",
"Risk": "Using **outdated PHP** enables exploitation of known flaws, including **remote code execution**, causing secret disclosure (confidentiality), unauthorized changes (integrity), and crashes or downtime (availability). Deprecated versions lack patches, widening exposure and instability.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-php.html",
"https://learn.microsoft.com/en-us/azure/app-service/configure-language-php?pivots=platform-linux#set-php-version",
"https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings"
],
"Remediation": {
"Code": {
"CLI": "az webapp config set --resource-group <resource group name> --name <app name> [--linux-fx-version <php runtime version>][--php-version <php version>]",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-php.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-php-version-is-the-latest-if-used-to-run-the-web-app#terraform"
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --linux-fx-version \"PHP|8.2\"",
"NativeIaC": "```bicep\n// Update App Service runtime to latest PHP\nresource appConfig 'Microsoft.Web/sites/config@2022-09-01' = {\n name: '<example_resource_name>/web'\n properties: {\n linuxFxVersion: 'PHP|8.2' // Critical: sets the app runtime to PHP 8.2 (latest) to pass the check\n }\n}\n```",
"Other": "1. In Azure Portal, go to App Services and select your app\n2. Navigate to Settings > Configuration > General settings\n3. Under Stack settings, select PHP and set Version to 8.2\n4. Click Save and confirm the restart",
"Terraform": "```hcl\n# Set latest PHP version on Linux Web App\nresource \"azurerm_linux_web_app\" \"app\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {\n application_stack {\n php_version = \"8.2\" # Critical: sets PHP to latest to pass the check\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home open the Portal Menu in the top left 2. Go to App Services 3. Click on each App 4. Under Settings section, click on Configuration 5. Click on the General settings pane, ensure that for a Stack of PHP the Major Version and Minor Version reflect the latest stable and supported release. NOTE: No action is required If PHP version is set to Off or is set with an empty value as PHP is not used by your web app",
"Url": "https://learn.microsoft.com/en-us/azure/app-service/configure-language-php?pivots=platform-linux#set-php-version"
"Text": "Standardize on the **latest supported PHP** and avoid EoL releases. Update promptly after security advisories, validate in staging, and automate version governance across apps. Prefer supported Linux runtimes, limit optional extensions, and apply **defense in depth** and **least privilege** to reduce blast radius.",
"Url": "https://hub.prowler.com/check/app_ensure_php_version_is_latest"
}
},
"Categories": [],
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "If your app is written using version-dependent features or libraries, they may not be available on the latest version. If you wish to upgrade, research the impact thoroughly. Upgrading may have unforeseen consequences that could result in downtime"
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_ensure_python_version_is_latest",
"CheckTitle": "Ensure that 'Python version' is the Latest Stable Version, if Used to Run the Web App",
"CheckTitle": "App Service web app uses the latest supported Python version or 3.12 by default",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Periodically, newer versions are released for Python software either due to security flaws or to include additional functionality. Using the latest full Python version for web apps is recommended in order to take advantage of security fixes, if any, and/or additional functionalities of the newer version.",
"Risk": "Newer versions may contain security enhancements and additional functionality. Using the latest software version is recommended in order to take advantage of enhancements and new capabilities. With each software installation, organizations need to determine if a given update meets their requirements. They must also verify the compatibility and support provided for any additional software against the update revision that is selected. Using the latest full version will keep your stack secure to vulnerabilities and exploits.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings",
"Description": "**Azure App Service web apps** using **Python** are assessed to confirm the runtime is the **latest supported version** (e.g., `3.12`). The evaluation reads the app's stack configuration to detect Python usage and compares the configured runtime against the defined latest baseline.",
"Risk": "Outdated **Python runtimes** weaken security and reliability:\n- Compromise confidentiality via known interpreter/SSL flaws\n- Undermine integrity through RCE and package exploitation\n- Reduce availability when deprecated versions lose patches and break under load",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-python.html",
"https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings",
"https://learn.microsoft.com/en-us/azure/app-service/configure-language-python#configure-python-version"
],
"Remediation": {
"Code": {
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> [--linux-fx-version 'PYTHON|3.12']",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-python.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-python-version-is-the-latest-if-used-to-run-the-web-app"
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --linux-fx-version \"PYTHON|3.12\"",
"NativeIaC": "```bicep\n// Set the Web App runtime to the latest Python version\nresource app 'Microsoft.Web/sites@2022-03-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource config 'Microsoft.Web/sites/config@2022-03-01' = {\n name: '${app.name}/web'\n properties: {\n linuxFxVersion: 'PYTHON|3.12' // Critical: sets Python runtime to 3.12 to pass the check\n }\n}\n```",
"Other": "1. In the Azure portal, go to App Services and select your app\n2. Go to Settings > Configuration > General settings\n3. Under Stack settings, set Python version to 3.12\n4. Click Save and confirm the restart",
"Terraform": "```hcl\n# Configure the Web App to use the latest Python version\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {\n application_stack {\n python_version = \"3.12\" # Critical: sets Python runtime to 3.12 to pass the check\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "From Azure Portal 1. From Azure Home open the Portal Menu in the top left 2. Go to App Services 3. Click on each App 4. Under Settings section, click on Configuration 5. Click on the General settings pane and ensure that the Major Version and the Minor Version is set to the latest stable version available (Python 3.11, at the time of writing) NOTE: No action is required if Python version is set to Off, as Python is not used by your web app.",
"Url": "https://learn.microsoft.com/en-us/azure/app-service/configure-language-python#configure-python-version"
"Text": "Adopt the **latest supported Python minor** for App Service and maintain a consistent upgrade policy. Track vendor EOL, test in staging, and roll out via CI/CD.\n\nApply **defense in depth**: minimize privileges and enforce strong TLS to reduce exposure during updates.",
"Url": "https://hub.prowler.com/check/app_ensure_python_version_is_latest"
}
},
"Categories": [],
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "If your app is written using version-dependent features or libraries, they may not be available on the latest version. If you wish to upgrade, research the impact thoroughly. Upgrading may have unforeseen consequences that could result in downtime."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_ensure_using_http20",
"CheckTitle": "Ensure that 'HTTP Version' is the Latest, if Used to Run the Web App",
"CheckTitle": "App Service web app has HTTP/2.0 enabled",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Periodically, newer versions are released for HTTP either due to security flaws or to include additional functionality. Using the latest HTTP version for web apps to take advantage of security fixes, if any, and/or new functionalities of the newer version.",
"Risk": "Newer versions may contain security enhancements and additional functionality. Using the latest version is recommended in order to take advantage of enhancements and new capabilities. With each software installation, organizations need to determine if a given update meets their requirements. They must also verify the compatibility and support provided for any additional software against the update revision that is selected. HTTP 2.0 has additional performance improvements on the head-of-line blocking problem of old HTTP version, header compression, and prioritization of requests. HTTP 2.0 no longer supports HTTP 1.1's chunked transfer encoding mechanism, as it provides its own, more efficient, mechanisms for data streaming.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings",
"Description": "**Azure App Service web apps** are evaluated for **HTTP/2 support** via the `http20_enabled` configuration, indicating whether the site serves traffic using the HTTP/2 protocol",
"Risk": "Without **HTTP/2**, apps remain on **HTTP/1.1**, increasing connection overhead and head-of-line blocking, which can reduce **availability** under load. Inefficient use of TLS sessions raises **DoS susceptibility** and degrades user experience, impacting service reliability",
"RelatedUrl": "",
"AdditionalURLs": [
"https://azure.microsoft.com/en-us/blog/announcing-http-2-support-in-azure-app-service/",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-http-2-for-app-service-web-applications.html",
"https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#general-settings"
],
"Remediation": {
"Code": {
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --http20-enabled true",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-http-2-for-app-service-web-applications.html",
"Terraform": ""
"NativeIaC": "```bicep\n// Enable HTTP/2.0 on an existing App Service web app\nresource webConfig 'Microsoft.Web/sites/config@2022-09-01' = {\n name: '<example_resource_name>/web'\n properties: {\n http20Enabled: true // Critical: enables HTTP/2.0 for the app\n }\n}\n```",
"Other": "1. Sign in to the Azure portal and go to App Services\n2. Select your web app\n3. Navigate to Settings > Configuration > General settings\n4. Set HTTP version to 2.0\n5. Click Save",
"Terraform": "```hcl\n# Enable HTTP/2.0 for an App Service (use azurerm_linux_web_app or azurerm_windows_web_app)\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<LOCATION>\"\n resource_group_name = \"<example_resource_group>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {\n http2_enabled = true # Critical: enables HTTP/2.0\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under Setting section, Click on Configuration 5. Set HTTP version to 2.0 under General settings",
"Url": "https://azure.microsoft.com/en-us/blog/announcing-http-2-support-in-azure-app-service/"
"Text": "Enable **HTTP/2** (`http20_enabled=true`) to use a modern, efficient transport.\n\n- Enforce `HTTPS Only` and a strong minimum `TLS` version for defense-in-depth\n- Validate app/library compatibility before rollout\n- Monitor performance and errors post-change; deploy gradually",
"Url": "https://hub.prowler.com/check/app_ensure_using_http20"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-posture-vulnerability-management#pv-7-rapidly-and-automatically-remediate-software-vulnerabilities"
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "app_ftp_deployment_disabled",
"CheckTitle": "Ensure FTP deployments are Disabled",
"CheckTitle": "App Service web app has FTP disabled or FTPS-only enforced",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites/config",
"Severity": "high",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "By default, Azure Functions, Web, and API Services can be deployed over FTP. If FTP is required for an essential deployment workflow, FTPS should be required for FTP login for all App Service Apps and Functions.",
"Risk": "Azure FTP deployment endpoints are public. An attacker listening to traffic on a wifi network used by a remote employee or a corporate network could see login traffic in clear-text which would then grant them full control of the code base of the app or service. This finding is more severe if User Credentials for deployment are set at the subscription level rather than using the default Application Credentials which are unique per App.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/deploy-ftp?tabs=portal",
"Description": "**Azure App Service web apps** are evaluated for **FTP exposure** via the `ftpsState` setting. Values `FtpsOnly` or `Disabled` indicate FTP is not allowed; `AllAllowed` means both FTP and FTPS are accepted.",
"Risk": "Allowing **FTP (unencrypted)** exposes credentials on public endpoints, enabling **credential theft** and **session hijacking**.\n\nCompromise grants write access to code and content, enabling **malicious deployments**, backdoors, and data leakage, degrading **integrity** and **confidentiality**-with greater blast radius if shared, user-scope publishing credentials are used.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/app-service/deploy-ftp?tabs=portal",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/ftp-access-disabled.html",
"https://learn.microsoft.com/en-gb/answers/questions/1323820/can-i-create-an-azure-policy-that-disables-both-ft",
"https://icompaas.freshdesk.com/support/solutions/articles/62000234759-ensure-ftp-state-is-set-to-ftps-only-or-disabled-"
],
"Remediation": {
"Code": {
"CLI": "az webapp config set --resource-group <resource group name> --name <app name> --ftps-state [disabled|FtpsOnly]",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/ftp-access-disabled.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-ftp-deployments-are-disabled#terraform"
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP> --name <APP_NAME> --ftps-state FtpsOnly",
"NativeIaC": "```bicep\n// Configure an existing App Service to enforce FTPS-only\nresource webConfig 'Microsoft.Web/sites/config@2022-03-01' = {\n name: '<example_resource_name>/web'\n properties: {\n ftpsState: 'FtpsOnly' // CRITICAL: Sets FTP state to FTPS-only, avoiding insecure 'AllAllowed'\n }\n}\n```",
"Other": "1. In Azure Portal, go to App Services and select your app\n2. Go to Settings > Configuration > General settings\n3. Set FTP state to FTPS only (or Disabled)\n4. Click Save",
"Terraform": "```hcl\n# Enforce FTPS-only on an App Service\nresource \"azurerm_windows_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_resource_name>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {\n ftps_state = \"FtpsOnly\" # CRITICAL: Enforces FTPS-only (not AllAllowed)\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Go to the Azure Portal 2. Select App Services 3. Click on an app 4. Select Settings and then Configuration 5. Under General Settings, for the Platform Settings, the FTP state should be set to Disabled or FTPS Only",
"Url": ""
"Text": "Disable FTP or enforce **FTPS** (`ftpsState: FtpsOnly` or `Disabled`).\n\nPrefer **CI/CD** over manual FTP and apply **least privilege** with app-scoped credentials. Rotate publishing secrets, enforce modern TLS, and restrict access via private networking. *If FTP is unavoidable*, require FTPS and monitor publishing logs.",
"Url": "https://hub.prowler.com/check/app_ftp_deployment_disabled"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Any deployment workflows that rely on FTP or FTPs rather than the WebDeploy or HTTPs endpoints may be affected."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "app_function_access_keys_configured",
"CheckTitle": "Ensure that Azure Functions are using access keys for enhanced security",
"CheckTitle": "Function app has function keys configured",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Azure Functions provide a way to secure HTTP function endpoints during development and production. Using access keys adds an extra layer of protection, ensuring that only authorized users or systems can access the functions. This is particularly important when dealing with public apps or sensitive data.",
"Risk": "Unprotected function endpoints may be vulnerable to unauthorized access, leading to potential data breaches or malicious activity.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-csharp#authorization-keys",
"Description": "**Azure Function apps** are evaluated for configured **function access keys** on HTTP endpoints.\n\nThe finding distinguishes functions with at least one access key defined from those without any keys configured.",
"Risk": "Missing **access keys** weakens authentication, enabling unsolicited calls to function endpoints. This risks:\n- loss of **confidentiality** via data exposure\n- compromised **integrity** by triggering unintended actions\n- reduced **availability** from abuse, throttling, and cost spikes",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/azure-functions/security-concepts?tabs=v4#function-access-keys",
"https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-anonymous-access.html"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az functionapp function keys set --resource-group <RESOURCE_GROUP> --name <FUNCTION_APP_NAME> --function-name <FUNCTION_NAME> --key-name default --key-value <KEY_VALUE>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-anonymous-access.html",
"Other": "1. Sign in to the Azure portal and go to your Function App\n2. Select Functions, then click the specific function\n3. Open Function keys (or API keys)\n4. Click Add (New function key), set Name (e.g., default) and value (or generate)\n5. Save to create the key",
"Terraform": ""
},
"Recommendation": {
"Text": "Use access keys to secure Azure Functions. You can create and manage keys in the Azure portal or using the Azure CLI. For more information, see the official documentation.",
"Url": "https://learn.microsoft.com/en-us/azure/azure-functions/security-concepts?tabs=v4#function-access-keys"
"Text": "Enforce **function keys** for non-public endpoints and apply **least privilege**:\n- avoid `anonymous` when not required\n- rotate keys; don't share the `admin` key\n- enable **App Service Authentication** or **API Management** for identity-aware access\n- restrict inbound networks and monitor logs\n- store and rotate secrets in **Key Vault**",
"Url": "https://hub.prowler.com/check/app_function_access_keys_configured"
}
},
"Categories": [],
"Categories": [
"identity-access",
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "For additional security, consider using managed identities and key vaults along with access keys. This provides granular control over resource access and improves auditability."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_application_insights_enabled",
"CheckTitle": "Ensure Function App has Application Insights configured",
"CheckTitle": "Function App has Application Insights configured",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites",
"Severity": "medium",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Application Insights is a powerful tool for monitoring the performance and health of Azure Function Apps. It provides valuable insights into exceptions, performance issues, and usage patterns, enabling timely detection and resolution of issues.",
"Risk": "Without Application Insights, you may miss critical errors, performance degradation, or abnormal behavior in your Function App, potentially impacting availability and user experience.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview",
"Description": "**Azure Function apps** are configured to send telemetry to **Application Insights** when application settings include `APPLICATIONINSIGHTS_CONNECTION_STRING` or `APPINSIGHTS_INSTRUMENTATIONKEY`.",
"Risk": "Without this telemetry, **visibility** into exceptions, dependencies, and performance is lost, reducing **availability** and delaying response. Gaps in traces mask anomalous traffic and failures, enabling prolonged outages and undermining **integrity** of processing (e.g., undetected retries or timeouts).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/azure-monitor/app/monitor-functions",
"https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/function-app-insights-on.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/function-app-insights-on.html",
"Terraform": ""
"CLI": "az functionapp config appsettings set --resource-group <RESOURCE_GROUP> --name <FUNCTION_APP_NAME> --settings APPLICATIONINSIGHTS_CONNECTION_STRING=<CONNECTION_STRING>",
"NativeIaC": "```bicep\n// Add Application Insights connection string to an existing Function App\nresource functionApp 'Microsoft.Web/sites@2022-09-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource appSettings 'Microsoft.Web/sites/config@2022-09-01' = {\n name: '${functionApp.name}/appsettings'\n properties: {\n APPLICATIONINSIGHTS_CONNECTION_STRING: '<CONNECTION_STRING>' // Critical: setting this enables Application Insights for the Function App\n }\n}\n```",
"Other": "1. In Azure Portal, go to Function App > Configuration > Application settings\n2. Click + New application setting\n3. Name: APPLICATIONINSIGHTS_CONNECTION_STRING\n4. Value: paste the connection string from your Application Insights resource (Overview > Connection string)\n5. Click OK, then Save\n6. If prompted, click Continue to apply the changes",
"Terraform": "```hcl\n# Add Application Insights connection string to an existing Function App via ARM deployment\nresource \"azurerm_resource_group_template_deployment\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<RESOURCE_GROUP>\"\n deployment_mode = \"Incremental\"\n\n template_content = jsonencode({\n \"$schema\" = \"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#\",\n \"contentVersion\" = \"1.0.0.0\",\n \"resources\" = [\n {\n \"type\" = \"Microsoft.Web/sites/config\",\n \"apiVersion\" = \"2022-09-01\",\n \"name\" = \"<FUNCTION_APP_NAME>/appsettings\",\n \"properties\" = {\n \"APPLICATIONINSIGHTS_CONNECTION_STRING\" = \"<CONNECTION_STRING>\" // Critical: setting this enables Application Insights for the Function App\n }\n }\n ]\n })\n}\n```"
},
"Recommendation": {
"Text": "Enable Application Insights for your Azure Function App to monitor its performance and health.",
"Url": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/monitor-functions"
"Text": "Enable **Application Insights** for each Function App using a `APPLICATIONINSIGHTS_CONNECTION_STRING` and standardize telemetry. Apply **defense in depth**: use distributed tracing, alert on errors/latency, and enforce least-privilege access and retention on logs to prevent blind spots and speed recovery.",
"Url": "https://hub.prowler.com/check/app_function_application_insights_enabled"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,31 +1,36 @@
{
"Provider": "azure",
"CheckID": "app_function_ftps_deployment_disabled",
"CheckTitle": "Ensure that FTP and FTPS deployments are disabled for Azure Functions to prevent unauthorized access and data breaches.",
"CheckTitle": "Function app has FTP and FTPS deployments disabled",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Azure FTP deployment endpoints are unencrypted and public, making them vulnerable to attacks. Disabling FTP and FTPS deployments enhances security by preventing unauthorized access to login credentials and sensitive codebases.",
"Risk": "If left enabled, attackers can intercept network traffic and gain full control of the app or service, leading to potential data breaches and unauthorized modifications.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/app-service/deploy-ftp",
"Description": "**Azure Function apps** are evaluated for the `ftps_state` setting that controls **FTP/FTPS deployment endpoints**. Values `AllAllowed` or `FtpsOnly` indicate deployment over FTP/FTPS is enabled, while `Disabled` indicates both endpoints are turned off.",
"Risk": "Enabled **FTP/FTPS deployment** undermines confidentiality and integrity. FTP exposes credentials in cleartext; FTPS still presents a public basic-auth endpoint susceptible to brute force and credential reuse. Compromise enables **unauthorized code pushes**, leading to RCE, data leakage, and service disruption.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies?tabs=windows#trigger-syncing",
"https://docs.microsoft.com/en-us/azure/app-service/deploy-ftp"
],
"Remediation": {
"Code": {
"CLI": "az webapp config set --resource-group <resource-group> --name <app-name> --ftps-state Disabled",
"NativeIaC": "",
"Other": "",
"Terraform": "",
"Arm": ""
"NativeIaC": "```bicep\n// Disable FTP and FTPS on an existing Function App\nresource functionApp 'Microsoft.Web/sites@2022-09-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource webConfig 'Microsoft.Web/sites/config@2022-09-01' = {\n name: 'web'\n parent: functionApp\n properties: {\n ftpsState: 'Disabled' // CRITICAL: Disables both FTP and FTPS deployments\n }\n}\n```",
"Other": "1. In the Azure portal, go to your Function App\n2. Select Configuration > General settings\n3. Under Platform settings, set FTP state to Disabled\n4. Click Save",
"Terraform": "```hcl\n# Disable FTP and FTPS on a Function App\nresource \"azurerm_linux_function_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n service_plan_id = \"<example_resource_id>\"\n storage_account_name = \"<example_resource_name>\"\n storage_account_access_key = \"<example_secret_value>\"\n functions_extension_version = \"~4\"\n\n site_config {\n ftps_state = \"Disabled\" # CRITICAL: Disables both FTP and FTPS deployments\n }\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to disable FTP and FTPS deployments for Azure Functions to mitigate security risks. Instead, consider using more secure deployment methods such as Docker contianer or enabling continuous deployment with GitHub Actions.",
"Url": "https://learn.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies?tabs=windows#trigger-syncing"
"Text": "Disable **FTP and FTPS deployment** on Function apps (`ftps_state: Disabled`). Adopt **defense in depth**: deploy via **CI/CD** with packaged artifacts (zip or containers), enforce **least privilege** publishing access, and limit exposure of build and deployment endpoints. *If unavoidable, use FTPS-only with TLS 1.2 and rotate credentials promptly.*",
"Url": "https://hub.prowler.com/check/app_function_ftps_deployment_disabled"
}
},
"Categories": [],
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check ensures that Azure Functions are deployed securely, reducing the attack surface and protecting sensitive information."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_identity_is_configured",
"CheckTitle": "Ensure Azure function has system or user assigned managed identity configured",
"CheckTitle": "Function app has a system-assigned or user-assigned managed identity enabled",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Azure Functions should have managed identities configured for enhanced security and access control.",
"Risk": "Not using managed identities can lead to less secure authentication and authorization practices, potentially exposing sensitive data.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview",
"Description": "**Azure Function Apps** are evaluated for an enabled **managed identity** (`SystemAssigned` or `UserAssigned`) configured on the app.\n\nThe finding indicates whether an identity is present to support token-based access to other Azure resources.",
"Risk": "Without **managed identities**, apps rely on stored secrets/keys, risking:\n- Confidentiality loss from leaked credentials\n- Integrity tampering via unauthorized writes\n- Availability outages from secret expiry/rotation\n\nCompromised keys enable unauthorized access and lateral movement.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-system-assigned-identity.html",
"https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity",
"https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview"
],
"Remediation": {
"Code": {
"CLI": "az functionapp identity assign --name <function_name> --resource-group <resource_group> --identities [system]",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-system-assigned-identity.html",
"Terraform": ""
"CLI": "az functionapp identity assign --resource-group <resource_group> --name <function_name>",
"NativeIaC": "```bicep\n// Enable managed identity on an existing Function App\nparam location string = resourceGroup().location\n\nresource functionApp 'Microsoft.Web/sites@2022-03-01' = {\n name: '<example_resource_name>'\n location: location\n identity: {\n type: 'SystemAssigned' // CRITICAL: Enables a system-assigned managed identity so the check passes\n }\n}\n```",
"Other": "1. In Azure Portal, go to your Function App\n2. Under Settings, select Identity\n3. On the System assigned tab, set Status to On\n4. Click Save",
"Terraform": "```hcl\n# Enable managed identity on an existing Function App via PATCH\nresource \"azapi_update_resource\" \"<example_resource_name>\" {\n type = \"Microsoft.Web/sites@2022-03-01\"\n resource_id = \"<example_resource_id>\"\n body = jsonencode({\n identity = {\n type = \"SystemAssigned\" # CRITICAL: Enables a system-assigned managed identity so the check passes\n }\n })\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to enable managed identities for Azure Functions to enhance security and access control. This allows the function app to easily access other Azure resources securely and with the appropriate permissions.",
"Url": "https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity"
"Text": "Enable a **managed identity** on each Function App (`SystemAssigned` per app, `UserAssigned` for shared/long-lived needs). Replace secrets with token-based access and grant only required RBAC roles (**least privilege**). Remove keys from settings, apply **separation of duties**, and monitor access as part of **defense in depth**.",
"Url": "https://hub.prowler.com/check/app_function_identity_is_configured"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_identity_without_admin_privileges",
"CheckTitle": "Ensure that your Azure functions are not configured with an identity with admin privileges",
"CheckTitle": "Function app managed identity is not assigned Owner, Contributor, User Access Administrator, or Role Based Access Control Administrator roles",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "It is important to ensure that Azure functions are not configured with administrative privileges to maintain the principle of least privilege and reduce the attack surface. By limiting the privileges of Azure functions, potential security risks and data leaks can be mitigated.",
"Risk": "If Azure functions are configured with administrative privileges, it increases the risk of unauthorized access, privilege escalation, and data breaches. Attackers can exploit these privileges to gain access to sensitive data and compromise the entire system.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview",
"Description": "**Azure Function apps** with managed identities are evaluated for assignments to broad **administrative roles**: **Owner**, **Contributor**, **User Access Administrator**, **RBAC Administrator**.\n\nThe finding highlights functions whose identity carries elevated permissions beyond normal runtime needs.",
"Risk": "Admin rights on a function's identity expose the control plane.\n- Confidentiality: read secrets and data\n- Integrity: alter configs, grant roles, deploy changes\n- Availability: stop or delete resources\nA runtime compromise can enable **lateral movement** and **privilege escalation** across the environment.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-admin-permissions.html",
"https://docs.microsoft.com/en-us/azure/architecture/framework/security/design-identity-authorization#use-the-principle-of-least-privilege",
"https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az role assignment delete --assignee <MANAGED_IDENTITY_PRINCIPAL_ID> --scope <SCOPE_ID>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-admin-permissions.html",
"Other": "1. In the Azure portal, open the scope where the role is assigned (e.g., Subscription, Resource group, or the Function App resource)\n2. Go to Access control (IAM) > Role assignments\n3. In the Principal filter, search for the Function App's managed identity (<function-app-name>)\n4. For each assignment with role Owner, Contributor, User Access Administrator, or Role Based Access Control Administrator, click Remove\n5. Repeat steps 1-4 at all relevant scopes (subscription, resource group, and Function App) until no such admin roles remain for this identity",
"Terraform": ""
},
"Recommendation": {
"Text": "To remediate this issue, ensure that Azure functions are not configured with an identity that has administrative privileges. Instead, use the principle of least privilege to grant only the necessary permissions to Azure functions. For more information, refer to the official documentation: Use the principle of least privilege.",
"Url": "https://docs.microsoft.com/en-us/azure/architecture/framework/security/design-identity-authorization#use-the-principle-of-least-privilege"
"Text": "Apply **least privilege**: grant only narrowly scoped, data-plane permissions needed by the function; avoid broad roles like `Owner` or `Contributor`.\nUse **separation of duties** and **just-in-time** elevation for rare admin tasks.\nRegularly review role assignments and restrict scope to the smallest necessary boundary.",
"Url": "https://hub.prowler.com/check/app_function_identity_without_admin_privileges"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check helps prevent privilege escalation attacks and ensures that Azure functions operate with the necessary permissions, reducing the impact of potential security breaches."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_latest_runtime_version",
"CheckTitle": "Ensure Azure Functions are using the latest supported runtime",
"CheckTitle": "Function app uses the latest supported runtime version (~4)",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites",
"Severity": "medium",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Keeping Azure Functions up to date with the latest supported runtime version is crucial for security and performance. Updates often include security patches and enhancements, helping to protect against known vulnerabilities and potential exploits. Additionally, newer runtime versions may offer improved functionality and optimized resource utilization.",
"Risk": "Using outdated runtime versions may introduce security risks and performance degradation. Outdated runtimes may have unpatched vulnerabilities, making them susceptible to attacks.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions",
"Description": "**Azure Function apps** are assessed for the **runtime version** set via `FUNCTIONS_EXTENSION_VERSION`. The finding identifies apps not configured to use the current supported major version `~4`.",
"Risk": "Outdated Functions runtimes erode CIA:\n- **Confidentiality**: known flaws enable unauthorized data access.\n- **Integrity**: RCE or binding bugs allow code tampering.\n- **Availability**: missing fixes cause crashes and scale faults.\n\nEnd-of-support versions (e.g., 2.x/3.x) lack security patches, increasing exploitability.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.microsoft.com/en-us/azure/azure-functions/functions-versions",
"https://learn.microsoft.com/en-us/azure/azure-functions/migrate-version-3-version-4?tabs=net8%2Cazure-cli%2Cwindows&pivots=programming-language-python",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-runtime-version.html"
],
"Remediation": {
"Code": {
"CLI": "az functionapp config appsettings set --name <function_app_name> --resource-group <resource_group_name> --settings FUNCTIONS_EXTENSION_VERSION=~4",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-runtime-version.html",
"Terraform": ""
"NativeIaC": "```bicep\n// Set Azure Functions runtime to v4 for an existing Function App\nresource functionApp 'Microsoft.Web/sites@2022-09-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource appSettings 'Microsoft.Web/sites/config@2022-09-01' = {\n name: '${functionApp.name}/appsettings'\n properties: {\n FUNCTIONS_EXTENSION_VERSION: '~4' // Critical: ensures the Function App uses runtime ~4\n }\n}\n```",
"Other": "1. In the Azure portal, go to Function App <example_resource_name>\n2. Select Configuration > Application settings\n3. Add or edit the setting:\n - Name: FUNCTIONS_EXTENSION_VERSION\n - Value: ~4\n4. Click Save and confirm the restart\n5. Verify the setting shows FUNCTIONS_EXTENSION_VERSION = ~4",
"Terraform": "```hcl\n# Minimal Function App with runtime set to v4 (~4) - use azurerm_linux_function_app or azurerm_windows_function_app\nresource \"azurerm_resource_group\" \"example\" {\n name = \"<example_resource_name>\"\n location = \"eastus\"\n}\n\nresource \"azurerm_storage_account\" \"example\" {\n name = \"<example_resource_name>\"\n resource_group_name = azurerm_resource_group.example.name\n location = azurerm_resource_group.example.location\n account_tier = \"Standard\"\n account_replication_type = \"LRS\"\n}\n\nresource \"azurerm_service_plan\" \"example\" {\n name = \"<example_resource_name>\"\n resource_group_name = azurerm_resource_group.example.name\n location = azurerm_resource_group.example.location\n os_type = \"Linux\"\n sku_name = \"Y1\"\n}\n\nresource \"azurerm_linux_function_app\" \"example\" {\n name = \"<example_resource_name>\"\n location = azurerm_resource_group.example.location\n resource_group_name = azurerm_resource_group.example.name\n service_plan_id = azurerm_service_plan.example.id\n storage_account_name = azurerm_storage_account.example.name\n storage_account_access_key = azurerm_storage_account.example.primary_access_key\n\n site_config {}\n\n app_settings = {\n FUNCTIONS_EXTENSION_VERSION = \"~4\" # Critical: ensures the Function App uses runtime ~4\n }\n}\n```"
},
"Recommendation": {
"Text": "",
"Url": "https://learn.microsoft.com/en-us/azure/azure-functions/migrate-version-3-version-4?tabs=net8%2Cazure-cli%2Cwindows&pivots=programming-language-python"
"Text": "Standardize on supported runtime `~4` and align language/extension versions.\n- Enforce upgrades in CI/CD and use staging to validate before rollout.\n- Apply **least privilege** for app identities and secrets.\n- Prefer automated patching and periodic reviews to avoid drift; avoid downgrades or indefinite minor pinning.",
"Url": "https://hub.prowler.com/check/app_function_latest_runtime_version"
}
},
"Categories": [],
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Stay informed about the latest security updates and patch releases for Azure Functions to maintain a secure and up-to-date environment."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_not_publicly_accessible",
"CheckTitle": "Ensure Azure Functions are not publicly accessible",
"CheckTitle": "Function app is not publicly accessible",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Azure Functions should not be exposed to the public internet. Restricting access helps protect applications from potential threats and reduces the attack surface.",
"Risk": "Exposing Azure Functions to the public internet increases the risk of unauthorized access, data breaches, and other security threats.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-functions/functions-networking-options",
"Description": "**Azure Function apps** are assessed for whether they are reachable from the public Internet. The evaluation considers the app's `publicNetworkAccess` state and the presence of access restrictions or private endpoints to limit inbound traffic.",
"Risk": "Public exposure allows unauthorized invocation, risking data disclosure and tampering (**confidentiality** and **integrity**). Attackers can brute-force tokens or abuse misconfigurations for remote execution. Unrestricted calls also enable abuse and DoS, driving cost and harming **availability**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/app-service/overview-access-restrictions",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-exposed.html",
"https://learn.microsoft.com/en-us/azure/azure-functions/functions-networking-options"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-exposed.html",
"Terraform": ""
"CLI": "az functionapp update --resource-group <RESOURCE_GROUP> --name <FUNCTION_APP_NAME> --set publicNetworkAccess=Disabled",
"NativeIaC": "```bicep\n// Disable public access by denying all unmatched traffic\nresource functionApp 'Microsoft.Web/sites@2022-09-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource siteConfig 'Microsoft.Web/sites/config@2022-09-01' = {\n name: '${functionApp.name}/web'\n properties: {\n ipSecurityRestrictionsDefaultAction: 'Deny' // Critical: blocks public access via default endpoint\n }\n}\n```",
"Other": "1. In the Azure portal, go to your Function App\n2. Select Networking\n3. Under Public access, set Public network access to Disabled\n4. Click Save",
"Terraform": "```hcl\n# Disable public network access for the Function App\nresource \"azurerm_linux_function_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n service_plan_id = azurerm_service_plan.main.id\n storage_account_name = azurerm_storage_account.main.name\n storage_account_access_key = azurerm_storage_account.main.primary_access_key\n\n public_network_access_enabled = false # Critical: disables public endpoint access\n}\n```"
},
"Recommendation": {
"Text": "Review the Azure Functions security guidelines and ensure that access restrictions are in place. Use Azure Private Link and Key Vault for enhanced security.",
"Url": "https://learn.microsoft.com/en-us/azure/app-service/overview-access-restrictions"
"Text": "Apply network isolation and least privilege:\n- Set `publicNetworkAccess=Disabled`\n- Use access restrictions for trusted IPs/VNets or **Private Endpoints**\n- Require strong auth (e.g., **Microsoft Entra ID**) over shared keys\n- Front with **API Management/WAF**\n- Keep secrets in **Key Vault** and monitor access logs",
"Url": "https://hub.prowler.com/check/app_function_not_publicly_accessible"
}
},
"Categories": [],
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_function_vnet_integration_enabled",
"CheckTitle": "Ensure Virtual Network Integration is Enabled for Azure Functions",
"CheckTitle": "Function app has Virtual Network integration enabled",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "function",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites",
"Severity": "medium",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Enabling Virtual Network Integration for Azure Functions provides an additional layer of security by restricting access to selected virtual network subnets. This helps to protect your Function Apps from unauthorized access and potential threats.",
"Risk": "Without Virtual Network Integration, your Function Apps may be exposed to the public internet, increasing the risk of unauthorized access and potential security breaches.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/azure-functions/functions-networking-options#virtual-network-integration",
"Description": "**Azure Function apps** configured with **Virtual Network integration** uses a chosen subnet so outbound traffic is routed via the VNet and can reach private or service-endpoint-secured resources.\n\nThe finding reflects whether a function app is associated with a subnet resource ID.",
"Risk": "Without VNet integration, function apps send egress directly to the public Internet and cannot reach private endpoints.\n\nThis weakens confidentiality and integrity by bypassing NSG/UDR controls, enables data exfiltration from compromised code, and may force exposing backends publicly, increasing attack surface.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-vnet-integration-on.html",
"https://docs.microsoft.com/en-us/azure/azure-functions/functions-networking-options#enable-virtual-network-integration",
"https://docs.microsoft.com/en-us/azure/azure-functions/functions-networking-options#virtual-network-integration"
],
"Remediation": {
"Code": {
"CLI": "az functionapp vnet-integration update --name <function_app_name> --resource-group <resource_group_name> --vnet <vnet_name> --subnet <subnet_name>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Functions/azure-function-vnet-integration-on.html",
"Terraform": ""
"CLI": "az functionapp vnet-integration add --name <function_app_name> --resource-group <resource_group_name> --vnet <vnet_name> --subnet <subnet_name>",
"NativeIaC": "```bicep\n// Enable VNet integration for an existing Function App\nresource vnetConn 'Microsoft.Web/sites/virtualNetworkConnections@2022-03-01' = {\n name: '<example_resource_name>/<example_resource_name>' // <function_app_name>/<vnet_name>\n properties: {\n subnetResourceId: '/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>/providers/Microsoft.Network/virtualNetworks/<example_resource_name>/subnets/<example_resource_name>' // CRITICAL: attaches the Function App to this subnet\n isSwift: true // CRITICAL: enables regional VNet (Swift) integration\n }\n}\n```",
"Other": "1. In the Azure portal, go to your Function App\n2. Select Networking > VNet Integration\n3. Click Add VNet\n4. Choose the target Virtual network and Subnet\n5. Click OK/Save to apply\n",
"Terraform": "```hcl\n# Enable VNet integration for an existing Function App\nresource \"azurerm_app_service_virtual_network_swift_connection\" \"<example_resource_name>\" {\n app_service_id = \"/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>/providers/Microsoft.Web/sites/<example_resource_name>\" # CRITICAL: target Function App resource ID\n subnet_id = \"/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>/providers/Microsoft.Network/virtualNetworks/<example_resource_name>/subnets/<example_resource_name>\" # CRITICAL: subnet to integrate with\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to enable Virtual Network Integration for Azure Functions to enhance security and protect against unauthorized access.",
"Url": "https://docs.microsoft.com/en-us/azure/azure-functions/functions-networking-options#enable-virtual-network-integration"
"Text": "Enable **Virtual Network integration** and attach function apps to a dedicated subnet to enforce **least privilege network access**.\n\nRoute egress through the VNet (e.g., `Route All`), apply **NSGs/UDRs**, and use **private endpoints** or service endpoints for dependencies. Restrict outbound traffic by default as part of **defense in depth**.",
"Url": "https://hub.prowler.com/check/app_function_vnet_integration_enabled"
}
},
"Categories": [],
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "app_http_logs_enabled",
"CheckTitle": "Ensure that logging for Azure AppService 'HTTP logs' is enabled",
"CheckTitle": "App Service web app has HTTP logs enabled in diagnostic settings",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Microsoft.Web/sites/config",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Enable AppServiceHTTPLogs diagnostic log category for Azure App Service instances to ensure all http requests are captured and centrally logged.",
"Risk": "Capturing web requests can be important supporting information for security analysts performing monitoring and incident response activities. Once logging, these logs can be ingested into SIEM or other central aggregation point for the organization.",
"RelatedUrl": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation",
"Description": "**Azure App Service web apps** diagnostic settings include **HTTP request logging** when the `AppServiceHTTPLogs` category (or the `allLogs` group) is enabled to capture web access events.",
"Risk": "Without **HTTP access logs**, visibility into requests is lost, hindering **detection** of brute force, probing, and injection attempts. This weakens **forensics** and reduces **confidentiality** and **integrity** by masking data access paths and blocking reliable incident timelines.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation",
"https://docs.microsoft.com/en-us/azure/app-service/troubleshoot-diagnostic-logs"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/azure/azure-logging-policies/ensure-that-app-service-enables-http-logging#terraform"
"CLI": "az monitor diagnostic-settings create --name <example_resource_name> --resource /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Web/sites/<APP_NAME> --workspace <LOG_ANALYTICS_WORKSPACE_RESOURCE_ID> --logs '[{\"category\":\"AppServiceHTTPLogs\",\"enabled\":true}]'",
"NativeIaC": "```bicep\n// Enable HTTP Logs for an existing App Service via Azure Monitor diagnostic setting\nresource app 'Microsoft.Web/sites@2022-03-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: '<example_resource_name>'\n scope: app\n properties: {\n workspaceId: '<example_resource_id>' // Destination Log Analytics workspace\n logs: [\n {\n category: 'AppServiceHTTPLogs' // Critical: enable the HTTP Logs category\n enabled: true // Critical: turns HTTP Logs on\n }\n ]\n }\n}\n```",
"Other": "1. In Azure Portal, go to your App Service > Monitoring > Diagnostic settings\n2. Click + Add diagnostic setting\n3. Under Logs, check AppServiceHTTPLogs (or select the allLogs category group)\n4. Choose a destination (Log Analytics workspace, Storage account, or Event Hub)\n5. Click Save",
"Terraform": "```hcl\n# Enable HTTP Logs for App Service via Azure Monitor diagnostic setting\nresource \"azurerm_monitor_diagnostic_setting\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n target_resource_id = \"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Web/sites/<example_resource_name>\"\n log_analytics_workspace_id = \"<example_resource_id>\" # Destination Log Analytics workspace\n\n log { # Critical: enables the HTTP Logs category\n category = \"AppServiceHTTPLogs\"\n enabled = true\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Go to App Services For each App Service: 2. Go to Diagnostic Settings 3. Click Add Diagnostic Setting 4. Check the checkbox next to 'HTTP logs' 5. Configure a destination based on your specific logging consumption capability (for example Stream to an event hub and then consuming with SIEM integration for Event Hub logging).",
"Url": "https://docs.microsoft.com/en-us/azure/app-service/troubleshoot-diagnostic-logs"
"Text": "Enable **diagnostic settings** with `AppServiceHTTPLogs` (or `allLogs`) and route logs to a centralized store. Enforce **least privilege**, retention, and tamper-resistant storage. Integrate with a **SIEM** for analytics and alerting, and periodically verify logging coverage across all apps and regions.",
"Url": "https://hub.prowler.com/check/app_http_logs_enabled"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Log consumption and processing will incur additional cost."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "app_minimum_tls_version_12",
"CheckTitle": "Ensure Web App is using the latest version of TLS encryption",
"CheckTitle": "App Service web app has minimum TLS version set to 1.2 or 1.3",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Web/sites/config",
"Severity": "medium",
"ResourceType": "microsoft.web/sites/config",
"ResourceGroup": "serverless",
"Description": "The TLS (Transport Layer Security) protocol secures transmission of data over the internet using standard encryption technology. Encryption should be set with the latest version of TLS. App service allows TLS 1.2 by default, which is the recommended TLS level by industry standards such as PCI DSS.",
"Risk": "App service currently allows the web app to set TLS versions 1.0, 1.1 and 1.2. It is highly recommended to use the latest TLS 1.2 version for web app secure connections.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-tls-versions",
"Description": "**Azure App Service web apps** are assessed for the configured minimum TLS version for HTTPS. The expected baseline is `1.2` or `1.3`; settings that permit lower versions indicate acceptance of legacy TLS during client negotiation.",
"Risk": "Allowing `TLS 1.0/1.1` enables protocol downgrades and weak cipher negotiation, exposing HTTPS traffic to **MITM** interception, credential theft, and tampering. This undermines the **confidentiality** and **integrity** of sessions and data in transit, and can enable account takeover via stolen tokens.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/+azure/app-service/overview-tls",
"https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-tls-versions",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-tls-encryption-in-use.html",
"https://icompaas.freshdesk.com/support/solutions/articles/62000234773-ensure-that-minimum-tls-version-is-set-to-tls-v1-2-or-higher"
],
"Remediation": {
"Code": {
"CLI": "az webapp config set --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME> --min-tls-version 1.2",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/latest-version-of-tls-encryption-in-use.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-networking-policies/bc_azr_networking_6#terraform"
"NativeIaC": "```bicep\n// Update existing App Service to enforce minimum TLS 1.2\nresource app 'Microsoft.Web/sites@2023-01-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource appConfig 'Microsoft.Web/sites/config@2023-01-01' = {\n name: '${app.name}/web'\n properties: {\n minTlsVersion: '1.2' // CRITICAL: Enforces minimum TLS version 1.2 to pass the check\n }\n}\n```",
"Other": "1. Sign in to Azure Portal and go to App Services\n2. Select your app\n3. Go to Settings > Configuration > General settings\n4. Set Minimum TLS Version to 1.2 (or 1.3 if available)\n5. Click Save",
"Terraform": "```hcl\n# Enforce minimum TLS 1.2 on an Azure Linux Web App\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_resource_name>\"\n service_plan_id = \"<example_resource_id>\"\n\n site_config {\n minimum_tls_version = \"1.2\" # CRITICAL: Enforces minimum TLS 1.2 to pass the check\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under Setting section, Click on TLS/SSL settings 5. Under the Bindings pane, ensure that Minimum TLS Version set to 1.2 under Protocol Settings",
"Url": "https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-3-encrypt-sensitive-data-in-transit"
"Text": "Enforce a minimum of `TLS 1.2` (prefer `1.3`) and disable `1.0/1.1`. Require **HTTPS-only**, enable HSTS, and align with modern cipher suites. Test client compatibility and phase out legacy agents. Document narrow exceptions with compensating controls to uphold **defense in depth** and prevent downgrades.",
"Url": "https://hub.prowler.com/check/app_minimum_tls_version_12"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "By default, TLS Version feature will be set to 1.2 when a new app is created using the command-line tool or Azure Portal console."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "app_register_with_identity",
"CheckTitle": "Ensure that Register with Azure Active Directory is enabled on App Service",
"CheckTitle": "App Service web app has a managed identity configured",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Web/sites",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "Managed service identity in App Service provides more security by eliminating secrets from the app, such as credentials in the connection strings. When registering with Azure Active Directory in App Service, the app will connect to other Azure services securely without the need for usernames and passwords.",
"Risk": "App Service provides a highly scalable, self-patching web hosting service in Azure. It also provides a managed identity for apps, which is a turn-key solution for securing access to Azure SQL Database and other Azure services.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad?tabs=workforce-tenant",
"Description": "**Azure App Service web apps** are configured with a **managed identity** (`identity`: `SystemAssigned` or `UserAssigned`) for token-based access to Azure resources without embedded credentials",
"Risk": "**Missing managed identity** drives reliance on stored secrets. Leaked credentials enable **unauthorized access** to SQL, Storage, or Key Vault, leading to **data exfiltration**, tampering, and lateral movement. Secret expiry or revocation can break connectivity, degrading **availability**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-integrate-azure-managed-service-identity",
"https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad?tabs=workforce-tenant",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-registration-with-microsoft-entra-id.html"
],
"Remediation": {
"Code": {
"CLI": "az webapp identity assign --resource-group <RESOURCE_GROUP_NAME> --name <APP_NAME>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AppService/enable-registration-with-microsoft-entra-id.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-iam-policies/bc_azr_iam_1#terraform"
"NativeIaC": "```bicep\n// Enable system-assigned managed identity on an existing App Service app\nresource app 'Microsoft.Web/sites@2022-09-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n identity: {\n type: 'SystemAssigned' // Critical: enables a managed identity for the app\n }\n}\n```",
"Other": "1. Sign in to the Azure portal\n2. Go to App Services and select your app\n3. Under Settings, select Identity\n4. On the System assigned tab, set Status to On\n5. Click Save and confirm",
"Terraform": "```hcl\n# Enable system-assigned managed identity on the App Service app (use azurerm_linux_web_app or azurerm_windows_web_app)\nresource \"azurerm_linux_web_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = azurerm_resource_group.<example_resource_name>.location\n resource_group_name = azurerm_resource_group.<example_resource_name>.name\n service_plan_id = azurerm_service_plan.<example_resource_name>.id\n\n site_config {}\n\n identity { # Critical: enables managed identity\n type = \"SystemAssigned\" # Creates a system-assigned identity for the app\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Login to Azure Portal using https://portal.azure.com 2. Go to App Services 3. Click on each App 4. Under Setting section, Click on Identity 5. Under the System assigned pane, set Status to On",
"Url": "https://learn.microsoft.com/en-us/azure/app-service/scenario-secure-app-authentication-app-service"
"Text": "Enable a **managed identity** and use it for all service-to-service access. Apply **least privilege** on target resources and eliminate secrets from code and app settings. Remove legacy credentials, rotate residual keys, and monitor usage for **defense in depth**. *Use system-assigned per app; user-assigned for reuse or separation.*",
"Url": "https://hub.prowler.com/check/app_register_with_identity"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "By default, Managed service identity via Azure AD is disabled."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "appinsights_ensure_is_configured",
"CheckTitle": "Ensure Application Insights are Configured.",
"CheckTitle": "Subscription has at least one Application Insights resource configured",
"CheckType": [],
"ServiceName": "appinsights",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Microsoft.Insights/components",
"ResourceType": "microsoft.insights/components",
"ResourceGroup": "monitoring",
"Description": "Application Insights within Azure act as an Application Performance Monitoring solution providing valuable data into how well an application performs and additional information when performing incident response. The types of log data collected include application metrics, telemetry data, and application trace logging data providing organizations with detailed information about application activity and application transactions. Both data sets help organizations adopt a proactive and retroactive means to handle security and performance related metrics within their modern applications.",
"Risk": "Configuring Application Insights provides additional data not found elsewhere within Azure as part of a much larger logging and monitoring program within an organization's Information Security practice. The types and contents of these logs will act as both a potential cost saving measure (application performance) and a means to potentially confirm the source of a potential incident (trace logging). Metrics and Telemetry data provide organizations with a proactive approach to cost savings by monitoring an application's performance, while the trace logging data provides necessary details in a reactive incident response scenario by helping organizations identify the potential source of an incident within their application.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview",
"Description": "**Azure subscription** contains at least one **Application Insights** resource collecting application telemetry (metrics, traces, logs) for monitored workloads.\n\nThe check determines whether telemetry collection exists at the subscription level, indicating that application monitoring is configured.",
"Risk": "If **Application Insights** is missing, applications run with reduced **observability**, limiting detection of anomalies and attacks.\n\nThis undermines **integrity** and accountability (fewer traces), degrades **availability** by slowing troubleshooting, and increases exposure to undetected data exfiltration or injection at the app layer.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview",
"https://www.tenable.com/audits/items/CIS_Microsoft_Azure_Foundations_v2.0.0_L2.audit:8a7a608d180042689ad9d3f16aa359f1"
],
"Remediation": {
"Code": {
"CLI": "az monitor app-insights component create --app <app name> --resource-group <resource group name> --location <location> --kind 'web' --retention-time <INT days to retain logs> --workspace <log analytics workspace ID> -- subscription <subscription ID>",
"NativeIaC": "",
"Other": "https://www.tenable.com/audits/items/CIS_Microsoft_Azure_Foundations_v2.0.0_L2.audit:8a7a608d180042689ad9d3f16aa359f1",
"Terraform": ""
"CLI": "az monitor app-insights component create --app <APP_NAME> --resource-group <RESOURCE_GROUP> --location <LOCATION> --application-type web --subscription <SUBSCRIPTION_ID>",
"NativeIaC": "```bicep\n// Create a minimal Application Insights resource\nresource appInsights 'Microsoft.Insights/components@2020-02-02' = {\n name: '<example_resource_name>'\n location: '<location>'\n properties: {\n Application_Type: 'web' // Critical: creates the App Insights component required to pass the check\n }\n}\n```",
"Other": "1. In the Azure portal, go to Azure Monitor > Application Insights\n2. Click Create\n3. Select a Subscription and Resource group\n4. Enter a Name and choose a Region\n5. Click Review + create, then Create\n6. Verify the resource appears under Application Insights in the selected subscription",
"Terraform": "```hcl\n# Critical: This resource creates an Application Insights component to satisfy the check\nresource \"azurerm_application_insights\" \"main\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n application_type = \"web\" # Critical: ensures creation of the component\n}\n```"
},
"Recommendation": {
"Text": "1. Navigate to Application Insights 2. Under the Basics tab within the PROJECT DETAILS section, select the Subscription 3. Select the Resource group 4. Within the INSTANCE DETAILS, enter a Name 5. Select a Region 6. Next to Resource Mode, select Workspace-based 7. Within the WORKSPACE DETAILS, select the Subscription for the log analytics workspace 8. Select the appropriate Log Analytics Workspace 9. Click Next:Tags > 10. Enter the appropriate Tags as Name, Value pairs. 11. Click Next:Review+Create 12. Click Create.",
"Url": ""
"Text": "Deploy **Application Insights** for all critical workloads and centralize data in a **Log Analytics workspace**. Configure actionable alerts and dashboards, enforce **least privilege** on telemetry, and set retention/export policies. Use private connectivity and appropriate sampling, and integrate with SIEM for **defense in depth**.",
"Url": "https://hub.prowler.com/check/appinsights_ensure_is_configured"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Because Application Insights relies on a Log Analytics Workspace, an organization will incur additional expenses when using this service."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "containerregistry_admin_user_disabled",
"CheckTitle": "Ensure admin user is disabled for Azure Container Registry",
"CheckTitle": "Container Registry admin user is disabled",
"CheckType": [],
"ServiceName": "containerregistry",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "ContainerRegistry",
"ResourceType": "microsoft.containerregistry/registries",
"ResourceGroup": "container",
"Description": "Ensure that the admin user is disabled and Role-Based Access Control (RBAC) is used instead since it could grant unrestricted access to the registry",
"Risk": "If the admin user is enabled, it may lead to unauthorized access to the container registry and its resources, which could compromise the confidentiality, integrity, and availability of the images stored within.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#admin-account",
"Description": "**Azure Container Registry** admin account configuration, confirming the built-in **admin user** is disabled so access relies on Microsoft Entra-based **RBAC** identities and scoped roles.",
"Risk": "Using a shared, always-valid **admin credential** grants full push/pull and lacks attribution. Compromise enables unauthorized image pulls (confidentiality), malicious pushes or tag changes (integrity), and deletions or lockout (availability), enabling supply-chain attacks and lateral movement.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#admin-account"
],
"Remediation": {
"Code": {
"CLI": "az acr update --name <RegistryName> --resource-group <ResourceGroupName> --admin-enabled false",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"NativeIaC": "```bicep\n// Azure Container Registry with admin user disabled\nresource acr 'Microsoft.ContainerRegistry/registries@2025-11-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n sku: {\n name: '<SKU_NAME>'\n }\n properties: {\n adminUserEnabled: false // Critical: disables the admin user to pass the check\n }\n}\n```",
"Other": "1. In Azure Portal, go to Container registries and select your registry\n2. Under Settings, open Access keys\n3. Set Admin user to Disabled\n4. Click Save",
"Terraform": "```hcl\nresource \"azurerm_container_registry\" \"example\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<location>\"\n sku = \"<SKU_NAME>\"\n\n admin_enabled = false # Critical: disables ACR admin user to pass the check\n}\n```"
},
"Recommendation": {
"Text": "Disable the admin user on Azure Container Registry through the Azure Portal: 1. Navigate to your Container Registry. 2. In the settings, select 'Access keys'. 3. Ensure the 'Admin user' checkbox is not ticked. For all actions relying on registry access, switch to using Role-Based Access Control.",
"Url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#admin-account"
"Text": "Disable the **admin account** and require Microsoft Entra-backed **RBAC**. Assign least-privilege roles to users, service principals, or managed identities. Prefer short-lived credentials, rotate any residual secrets, and apply defense-in-depth with network restrictions and continuous auditing of registry access.",
"Url": "https://hub.prowler.com/check/containerregistry_admin_user_disabled"
}
},
"Categories": [],
"Categories": [
"identity-access",
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "The transition away from using the admin user to RBAC will facilitate a more secure and manageable access model, minimizing the potential risk of unauthorized access to your container images."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "containerregistry_not_publicly_accessible",
"CheckTitle": "Restrict public network access to the Container Registry",
"CheckTitle": "Container Registry public network access is disabled",
"CheckType": [],
"ServiceName": "containerregistry",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "ContainerRegistry",
"ResourceType": "microsoft.containerregistry/registries",
"ResourceGroup": "container",
"Description": "Ensure that public network access to the Azure Container Registry is restricted.",
"Risk": "Public accessibility exposes the Container Registry to potential attacks, unauthorized usage, and data breaches. Restricting access minimizes the surface area for attacks and ensures that only authorized networks can access the registry.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-access-selected-networks",
"Description": "**Azure Container Registry** configuration indicates whether the registry permits **unrestricted public access** based on the `Public network access` setting.",
"Risk": "**Internet-exposed ACR** expands attack paths impacting **CIA**:\n- Confidentiality: unauthorized image pulls leak code/secrets\n- Integrity: compromised creds allow tampered image pushes (supply-chain)\n- Availability: pull storms or scans exhaust quotas, causing outages and cost spikes",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ContainerRegistry/disable-public-access.html",
"https://learn.microsoft.com/en-us/azure/container-registry/container-registry-access-selected-networks"
],
"Remediation": {
"Code": {
"CLI": "az acr update --name <registry-name> --default-action Deny",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az acr update --name <registry-name> --public-network-enabled false",
"NativeIaC": "```bicep\n// Azure Container Registry with public network access disabled\nresource <example_resource_name> 'Microsoft.ContainerRegistry/registries@2025-11-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n sku: {\n name: 'Basic'\n }\n properties: {\n publicNetworkAccess: 'Disabled' // Critical: disables the public endpoint to prevent unrestricted access\n }\n}\n```",
"Other": "1. In the Azure portal, go to your Container Registry\n2. Select Settings > Networking\n3. On Public access, set Allow public network access to Disabled\n4. Click Save",
"Terraform": "```hcl\nresource \"azurerm_container_registry\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<location>\"\n sku = \"Basic\"\n\n public_network_access_enabled = false # Critical: disables public endpoint to block unrestricted access\n}\n```"
},
"Recommendation": {
"Text": "Ensure that the necessary virtual network configurations or IP rules are in place to allow access from required services once public access is restricted. Review the network access settings regularly to maintain a secure environment. To restrict public network access to your Azure Container Registry: 1. Navigate to your Container Registry in the Azure Portal. 2. Under 'Settings'->'Networking', configure the 'Public network access' settings to 'Disabled'. 3. Set up virtual network service endpoints or private endpoints as needed for secure access. 4. Review and adjust IP access rules as necessary.",
"Url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-access-selected-networks"
"Text": "Set `Public network access` to `Disabled` and use **Private Link** for registry access.\n\nIf public reachability is required, allow only **selected IPs**, enforce **least privilege** and token rotation, and apply **defense in depth** (egress control, network segmentation, logging of push/pull events).",
"Url": "https://hub.prowler.com/check/containerregistry_not_publicly_accessible"
}
},
"Categories": [],
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This feature is only available for Premium SKU registries."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "containerregistry_uses_private_link",
"CheckTitle": "Ensure to use a private link for accessing the Azure Container Registry",
"CheckTitle": "Container Registry uses a private endpoint (Private Link)",
"CheckType": [],
"ServiceName": "containerregistry",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "ContainerRegistry",
"Severity": "high",
"ResourceType": "microsoft.containerregistry/registries",
"ResourceGroup": "container",
"Description": "Ensure that a private link is used for accessing the Azure Container Registry to enhance security and restrict access to the registry over the public internet.",
"Risk": "Without using a private link, the Azure Container Registry may be exposed to the public internet, increasing the risk of unauthorized access and potential data breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/private-link/private-link-overview",
"Description": "**Azure Container Registry** access via **Private Endpoints** (Azure Private Link). Registries with `private endpoint connections` use private IPs; others rely on the public endpoint.",
"Risk": "Publicly reachable registries expand attack surface for **credential stuffing**, token abuse, and scanning. A compromise enables unauthorized pull/push, causing image **data leakage** and **supply-chain tampering**. Public routing weakens network isolation, impacting the **confidentiality** and **integrity** of images and metadata.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/private-link/private-link-overview",
"https://learn.microsoft.com/en-us/azure/container-registry/container-registry-vnet",
"https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link"
],
"Remediation": {
"Code": {
"CLI": "az network private-endpoint create --connection-name <ConnectionName> --resource-group <ResourceGroupName> --name <Name> --private-connection-resource-id <RegistryId> --vnet-name <VnetName> --subnet <SubnetName> --group-ids registry",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az network private-endpoint create --resource-group <example_resource_name> --name <example_resource_name> --vnet-name <example_resource_name> --subnet <example_resource_name> --private-connection-resource-id <example_resource_id> --group-ids registry --connection-name <example_resource_name>",
"NativeIaC": "```bicep\n// Create a Private Endpoint to ACR\nresource privateEndpoint 'Microsoft.Network/privateEndpoints@2025-05-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n subnet: {\n id: '<example_subnet_id>'\n }\n privateLinkServiceConnections: [\n {\n name: '<example_resource_name>'\n properties: {\n privateLinkServiceId: '<example_resource_id>' // Critical: ACR resource ID to connect\n groupIds: ['registry'] // Critical: Target the 'registry' subresource to enable Private Link\n }\n }\n ]\n }\n}\n```",
"Other": "1. In Azure Portal, go to Container registries > select your registry\n2. Navigate to Settings > Networking > Private endpoints tab\n3. Click + Private endpoint, enter a name, select your VNet and Subnet\n4. Set Resource type to Microsoft.ContainerRegistry/registries and Target subresource to registry\n5. Click Review + create, then Create",
"Terraform": "```hcl\nresource \"azurerm_private_endpoint\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n subnet_id = \"<example_subnet_id>\"\n\n private_service_connection {\n name = \"<example_resource_name>\"\n private_connection_resource_id = \"<example_resource_id>\" # Critical: ACR resource ID\n subresource_names = [\"registry\"] # Critical: Target 'registry' subresource to enable Private Link\n }\n}\n```"
},
"Recommendation": {
"Text": "Create a private link for Azure Container Registry through the Azure Portal: 1. Navigate to your Container Registry. 2. In the settings, select 'Networking'. 3. Select 'Private access'. 4. Configure a private endpoint for the registry.",
"Url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link"
"Text": "Use **Private Link** with **private endpoints** and set `Public network access: Disabled`.\n- Restrict access to trusted VNets/subnets\n- Prefer private endpoints over service endpoints\n- Enforce **least privilege** on registry actions\n- Configure private DNS for the registry FQDN\n- Monitor access logs for **defense in depth**",
"Url": "https://hub.prowler.com/check/containerregistry_uses_private_link"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This feature is only available for Premium SKU registries."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "cosmosdb_account_firewall_use_selected_networks",
"CheckTitle": "Ensure That 'Firewalls & Networks' Is Limited to Use Selected Networks Instead of All Networks",
"CheckTitle": "Cosmos DB account firewall allows access only from selected networks",
"CheckType": [],
"ServiceName": "cosmosdb",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "CosmosDB",
"ResourceType": "microsoft.documentdb/databaseaccounts",
"ResourceGroup": "database",
"Description": "Limiting your Cosmos DB to only communicate on whitelisted networks lowers its attack footprint.",
"Risk": "Selecting certain networks for your Cosmos DB to communicate restricts the number of networks including the internet that can interact with what is stored within the database.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-configure-private-endpoints",
"Description": "**Azure Cosmos DB accounts** limit connectivity to **selected networks** using virtual network rules and/or IP allowlists rather than permitting access from all networks.\n\nThe evaluation determines whether the account's network firewall enforces this restriction.",
"Risk": "Access from all networks enlarges the attack surface. If keys or tokens are exposed or privileges are misconfigured, attackers anywhere can read or modify data, harming **confidentiality** and **integrity**.\n\nWeak segmentation also enables SSRF/pivot paths from Azure services and can impact **availability** through abuse.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-configure-vnet-service-endpoint",
"https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-configure-firewall",
"https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security?tabs=azure-portal"
],
"Remediation": {
"Code": {
"CLI": "az cosmosdb database list / az cosmosdb show <database id> **isVirtualNetworkFilterEnabled should be set to true**",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az cosmosdb network-rule add -g <RESOURCE_GROUP> -n <ACCOUNT_NAME> --subnet <example_resource_id>",
"NativeIaC": "```bicep\n// Enable selected networks only by turning on VNet filter and adding one allowed subnet\nresource cosmos 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = {\n name: '<example_resource_name>'\n location: '<LOCATION>'\n properties: {\n databaseAccountOfferType: 'Standard'\n locations: [{ locationName: '<LOCATION>'; failoverPriority: 0 }]\n isVirtualNetworkFilterEnabled: true // CRITICAL: Enables VNet firewall (selected networks only)\n virtualNetworkRules: [\n {\n id: '<example_resource_id>' // CRITICAL: Subnet resource ID allowed to access the account\n }\n ]\n }\n}\n```",
"Other": "1. In Azure Portal, open your Cosmos DB account\n2. Go to Settings > Networking\n3. Select Selected networks\n4. Click Add existing virtual network, choose the VNet and Subnet, then click Enable and Add\n5. Click Save",
"Terraform": "```hcl\n# Enable Cosmos DB VNet firewall and allow a specific subnet\nresource \"azurerm_cosmosdb_account\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = azurerm_resource_group.<example_resource_name>.location\n resource_group_name = azurerm_resource_group.<example_resource_name>.name\n offer_type = \"Standard\"\n kind = \"GlobalDocumentDB\"\n\n consistency_policy { consistency_level = \"Session\" }\n geo_location { location = azurerm_resource_group.<example_resource_name>.location failover_priority = 0 }\n\n is_virtual_network_filter_enabled = true # CRITICAL: Enforces selected networks only\n virtual_network_rule {\n id = \"<example_resource_id>\" # CRITICAL: Subnet resource ID allowed to access the account\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Open the portal menu. 2. Select the Azure Cosmos DB blade. 3. Select a Cosmos DB account to audit. 4. Select Networking. 5. Under Public network access, select Selected networks. 6. Under Virtual networks, select + Add existing virtual network or + Add a new virtual network. 7. For existing networks, select subscription, virtual network, subnet and click Add. For new networks, provide a name, update the default values if required, and click Create. 8. Click Save.",
"Url": "https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security?tabs=azure-portal"
"Text": "Set network access to `Selected networks` with **least privilege**:\n- Prefer **private endpoints** or VNet service endpoints with subnet ACLs\n- Keep IP allowlists minimal; avoid `0.0.0.0`\n- *When feasible*, set `publicNetworkAccess=Disabled` with Private Link\n- Apply **defense in depth** and monitor access and firewall changes",
"Url": "https://hub.prowler.com/check/cosmosdb_account_firewall_use_selected_networks"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Failure to whitelist the correct networks will result in a connection loss."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "cosmosdb_account_use_aad_and_rbac",
"CheckTitle": "Use Azure Active Directory (AAD) Client Authentication and Azure RBAC where possible.",
"CheckTitle": "Cosmos DB account has local authentication disabled and uses Azure AD authentication with Azure RBAC",
"CheckType": [],
"ServiceName": "cosmosdb",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "CosmosDB",
"Severity": "high",
"ResourceType": "microsoft.documentdb/databaseaccounts",
"ResourceGroup": "database",
"Description": "Cosmos DB can use tokens or AAD for client authentication which in turn will use Azure RBAC for authorization. Using AAD is significantly more secure because AAD handles the credentials and allows for MFA and centralized management, and the Azure RBAC better integrated with the rest of Azure.",
"Risk": "AAD client authentication is considerably more secure than token-based authentication because the tokens must be persistent at the client. AAD does not require this.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/cosmos-db/role-based-access-control",
"Description": "**Azure Cosmos DB accounts** configured to use **Microsoft Entra ID** with **Azure RBAC** by disabling key-based credentials (`disableLocalAuth=true`). Clients authenticate with identities rather than account keys.",
"Risk": "With local/key-based auth enabled, **long-lived account keys** can be leaked or shared, enabling unauthorized reads/writes and tampering. Access bypasses **MFA** and granular **RBAC**, hindering rotation/revocation and increasing persistence and lateral movement risks.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-connect-role-based-access-control?pivots=azure-cli"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az resource update --resource-group <RESOURCE_GROUP> --name <COSMOS_ACCOUNT_NAME> --resource-type Microsoft.DocumentDB/databaseAccounts --set properties.disableLocalAuth=true",
"NativeIaC": "```bicep\n// Bicep: Disable local (key-based) auth on a Cosmos DB account\nresource account 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n kind: 'GlobalDocumentDB'\n properties: {\n databaseAccountOfferType: 'Standard'\n locations: [{ locationName: resourceGroup().location }]\n disableLocalAuth: true // Critical: Disables key-based auth to enforce Entra ID + Azure RBAC\n }\n}\n```",
"Other": "1. Sign in to the Azure portal and open your Cosmos DB account\n2. In the left menu, select Keys\n3. Turn on Disable key-based authentication (Disable local authentication)\n4. Click Save",
"Terraform": "```hcl\n# Terraform: Disable local (key-based) auth on a Cosmos DB account\nresource \"azurerm_cosmosdb_account\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_resource_name>\"\n offer_type = \"Standard\"\n kind = \"GlobalDocumentDB\"\n\n geo_location {\n location = \"<example_resource_name>\"\n failover_priority = 0\n }\n\n local_authentication_disabled = true # Critical: Disables key-based auth to enforce Entra ID + RBAC\n}\n```"
},
"Recommendation": {
"Text": "Map all the resources that currently access to the Azure Cosmos DB account with keys or access tokens. Create an Azure Active Directory (AAD) identity for each of these resources: For Azure resources, you can create a managed identity . You may choose between system-assigned and user-assigned managed identities. For non-Azure resources, create an AAD identity. Grant each AAD identity the minimum permission it requires. When possible, we recommend you use one of the 2 built-in role definitions: Cosmos DB Built-in Data Reader or Cosmos DB Built-in Data Contributor. Validate that the new resource is functioning correctly. After new permissions are granted to identities, it may take a few hours until they propagate. When all resources are working correctly with the new identities, continue to the next step. You can use the az resource update powershell command: $cosmosdbname = 'cosmos-db-account-name' $resourcegroup = 'resource-group-name' $cosmosdb = az cosmosdb show --name $cosmosdbname --resource-group $resourcegroup | ConvertFrom-Json az resource update --ids $cosmosdb.id --set properties.disableLocalAuth=true --latest- include-preview",
"Url": "https://learn.microsoft.com/en-us/azure/cosmos-db/role-based-access-control"
"Text": "Disable local authentication by setting `disableLocalAuth=true` and require **Entra ID + Azure RBAC** for control and data access. Use **managed identities**, apply **least privilege** roles, retire shared keys, and enforce **zero trust** with conditional access and short-lived credentials.",
"Url": "https://hub.prowler.com/check/cosmosdb_account_use_aad_and_rbac"
}
},
"Categories": [],
"Categories": [
"identity-access",
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "cosmosdb_account_use_private_endpoints",
"CheckTitle": "Ensure That Private Endpoints Are Used Where Possible",
"CheckTitle": "Cosmos DB account uses private endpoint connections",
"CheckType": [],
"ServiceName": "cosmosdb",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "CosmosDB",
"Severity": "high",
"ResourceType": "microsoft.documentdb/databaseaccounts",
"ResourceGroup": "database",
"Description": "Private endpoints limit network traffic to approved sources.",
"Risk": "For sensitive data, private endpoints allow granular control of which services can communicate with Cosmos DB and ensure that this network traffic is private. You set this up on a case by case basis for each service you wish to be connected.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-configure-private-endpoints",
"Description": "**Azure Cosmos DB accounts** are assessed for **private endpoint connections** that keep data-plane traffic on private IPs within authorized virtual networks.",
"Risk": "Without **private endpoints**, access may use public endpoints or broad IP rules, enabling:\n- interception and credential replay\n- unauthorized queries and data exfiltration\n- lateral movement via exposed paths\n\nThis degrades **confidentiality** and can impact **availability** under abusive traffic.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-configure-private-endpoints?tabs=arm-bicep",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/CosmosDB/use-private-endpoints.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az network private-endpoint create --name <example_resource_name> --resource-group <example_resource_name> --vnet-name <example_resource_name> --subnet <example_resource_name> --private-connection-resource-id /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<example_resource_name>/providers/Microsoft.DocumentDB/databaseAccounts/<example_resource_name> --group-ids Sql --connection-name <example_resource_name>",
"NativeIaC": "```bicep\n// Create a Private Endpoint to a Cosmos DB account (adds a private endpoint connection)\nresource pe 'Microsoft.Network/privateEndpoints@2025-05-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n subnet: { id: '<example_subnet_id>' }\n privateLinkServiceConnections: [\n {\n name: 'conn'\n properties: {\n privateLinkServiceId: '<example_cosmosdb_account_id>' // CRITICAL: attaches PE to the Cosmos DB account\n groupIds: ['Sql'] // CRITICAL: targets Cosmos DB NoSQL subresource so the connection is created\n }\n }\n ]\n }\n}\n```",
"Other": "1. In Azure Portal, open your Cosmos DB account\n2. Go to Networking > Private access\n3. Click + Private endpoint\n4. Resource type: Microsoft.AzureCosmosDB/databaseAccounts; Resource: select your account; Target subresource: Sql\n5. Select your Virtual network and Subnet\n6. Click Review + create, then Create\n7. Verify the private endpoint connection appears under Networking > Private access",
"Terraform": "```hcl\n# Create a Private Endpoint to a Cosmos DB account (adds a private endpoint connection)\nresource \"azurerm_private_endpoint\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n subnet_id = \"<example_subnet_id>\"\n\n private_service_connection {\n name = \"<example_resource_name>\"\n private_connection_resource_id = \"<example_cosmosdb_account_id>\" # CRITICAL: Cosmos DB account ID\n subresource_names = [\"Sql\"] # CRITICAL: targets Cosmos DB subresource to create the connection\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Open the portal menu. 2. Select the Azure Cosmos DB blade. 3. Select the Azure Cosmos DB account. 4. Select Networking. 5. Select Private access. 6. Click + Private Endpoint. 7. Provide a Name. 8. Click Next. 9. From the Resource type drop down, select Microsoft.AzureCosmosDB/databaseAccounts. 10. From the Resource drop down, select the Cosmos DB account. 11. Click Next. 12. Provide appropriate Virtual Network details. 13. Click Next. 14. Provide appropriate DNS details. 15. Click Next. 16. Optionally provide Tags. 17. Click Next : Review + create. 18. Click Create.",
"Url": "https://docs.microsoft.com/en-us/azure/private-link/tutorial-private-endpoint-cosmosdb-portal"
"Text": "Adopt **Azure Private Link** for Cosmos DB:\n- Create private endpoints for required subresources\n- Link a private DNS zone so clients resolve to private IPs\n- Set `PublicNetworkAccess=Disabled`; keep tight firewall rules\n- Allow only needed VNets/subnets; apply NSGs\n- Enforce least privilege and monitor access patterns",
"Url": "https://hub.prowler.com/check/cosmosdb_account_use_private_endpoints"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Only whitelisted services will have access to communicate with the Cosmos DB."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "databricks_workspace_cmk_encryption_enabled",
"CheckTitle": "Ensure Azure Databricks workspaces use customer-managed keys (CMK) for encryption at rest",
"CheckTitle": "Databricks workspace uses a customer-managed key (CMK) for encryption at rest",
"CheckType": [],
"ServiceName": "databricks",
"SubServiceName": "workspace",
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Databricks/workspaces/{workspaceName}",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureDatabricksWorkspace",
"ResourceType": "microsoft.databricks/workspaces",
"ResourceGroup": "ai_ml",
"Description": "Checks whether Azure Databricks workspaces are configured to use customer-managed keys (CMK) for encryption at rest, providing greater control over data encryption and compliance.",
"Risk": "Without CMK, organizations have less control over encryption keys, which may impact regulatory compliance and increase risk of unauthorized data access.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/databricks/security/keys/customer-managed-keys",
"Description": "**Azure Databricks workspaces** are evaluated for use of **customer-managed keys** (`CMK`) on at-rest encryption, based on the workspace's managed disk encryption configuration.",
"Risk": "Without **CMK**, keys are provider-controlled, degrading **confidentiality** and incident response.\n- Slower revoke/rotate during breaches\n- Weaker **separation of duties** and audit trails\n- Larger blast radius if storage or control plane is compromised",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Databricks/enable-encryption-with-cmk.html",
"https://learn.microsoft.com/en-us/azure/databricks/security/keys/customer-managed-keys"
],
"Remediation": {
"Code": {
"CLI": "az databricks workspace update --name <databricks-workspace-name> --resource-group <resource-group-name> --prepare-encryption && databricks workspace update --name <databricks-workspace-name> --resource-group <resource-group-name> --key-source 'Microsoft.KeyVault' --key-name <key-name> --key-vault <key-vault-uri> --key-version <key-version>",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az databricks workspace update --name <workspace_name> --resource-group <resource_group_name> --key-source Microsoft.Keyvault --key-name <key_name> --key-vault https://<key_vault_name>.vault.azure.net/ --key-version <key_version>",
"NativeIaC": "```bicep\nresource ws 'Microsoft.Databricks/workspaces@2023-02-01' = {\n name: '<example_resource_name>'\n location: '<region>'\n sku: {\n name: 'premium'\n }\n properties: {\n encryption: {\n keySource: 'Microsoft.Keyvault' // CRITICAL: enables CMK from Key Vault\n managedDiskKeyVaultProperties: { // CRITICAL: sets CMK for managed disks (encryption at rest)\n keyVaultUri: 'https://<key_vault_name>.vault.azure.net/'\n keyName: '<key_name>'\n keyVersion: '<key_version>'\n }\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to your Databricks workspace\n2. Select Settings > Encryption (or Customer-managed keys)\n3. If prompted, click Prepare encryption and wait for completion\n4. Set Key source to Microsoft Key Vault\n5. Select the Key Vault key and specific key version for managed disks\n6. Save to apply customer-managed key encryption",
"Terraform": "```hcl\nresource \"azurerm_databricks_workspace\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<region>\"\n sku = \"premium\"\n\n customer_managed_key_enabled = true # CRITICAL: enable CMK\n managed_disk_cmk_key_vault_key_id = \"<example_resource_id>\" # CRITICAL: key ID (Key Vault key) for managed disks\n}\n```"
},
"Recommendation": {
"Text": "Enable customer-managed keys (CMK) for Databricks workspaces using Azure Key Vault to enhance control over data encryption, auditing, and compliance.",
"Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Databricks/enable-encryption-with-cmk.html"
"Text": "Enable `CMK` for workspace encryption via **Key Vault** or **Managed HSM** and enforce:\n- Least privilege for key usage\n- Regular rotation and retire old versions\n- Audit logging and alerts on key ops\n- Separation of duties for key vs data roles\n- Deny-by-default policies limiting scope",
"Url": "https://hub.prowler.com/check/databricks_workspace_cmk_encryption_enabled"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Customer-managed key (CMK) encryption is only available for Databricks workspaces on the Premium tier."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "databricks_workspace_vnet_injection_enabled",
"CheckTitle": "Ensure Azure Databricks workspaces are deployed in a customer-managed VNet (VNet Injection)",
"CheckTitle": "Databricks workspace is deployed in a customer-managed VNet (VNet Injection enabled)",
"CheckType": [],
"ServiceName": "databricks",
"SubServiceName": "",
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Databricks/workspaces/{workspaceName}",
"Severity": "medium",
"ResourceType": "AzureDatabricksWorkspace",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "microsoft.databricks/workspaces",
"ResourceGroup": "ai_ml",
"Description": "Checks whether Azure Databricks workspaces are deployed in a customer-managed Virtual Network (VNet Injection) instead of a Databricks-managed VNet.",
"Risk": "Using a Databricks-managed VNet limits control over network security policies, firewall configurations, and routing, increasing the risk of unauthorized access or data exfiltration.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/databricks/administration-guide/cloud-configurations/azure/vnet-inject",
"Description": "**Azure Databricks workspaces** using **VNet injection** are placed in a customer-managed VNet rather than a Databricks-managed network. This evaluates whether a workspace is linked to a customer VNet.",
"Risk": "Using a Databricks-managed VNet limits control over routing, egress, and access boundaries, degrading **confidentiality** and **integrity**.\n- Unrestricted outbound paths enable **data exfiltration**\n- Harder to enforce **private endpoints** and NSG policies\n- Increased chance of **lateral movement** into compute nodes",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/databricks/administration-guide/cloud-configurations/azure/vnet-inject",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Databricks/check-for-vnet-injection.html"
],
"Remediation": {
"Code": {
"CLI": "az databricks workspace create --name <databricks-workspace-name> --resource-group <resource-group-name> --location <region> --managed-resource-group <managed-rg-name> --enable-no-public-ip true --network-security-group-rule \"NoAzureServices\" --public-network-access Disabled --custom-virtual-network-id /subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Network/virtualNetworks/<vnet-name>",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az databricks workspace create --name <workspace_name> --resource-group <resource_group_name> --location <region> --sku premium --vnet /subscriptions/<subscription_id>/resourceGroups/<resource_group_name>/providers/Microsoft.Network/virtualNetworks/<vnet_name> --public-subnet <public_subnet_name> --private-subnet <private_subnet_name>",
"NativeIaC": "```bicep\n// Azure Databricks workspace with VNet injection enabled\nresource databricks 'Microsoft.Databricks/workspaces@2023-02-01-preview' = {\n name: '<example_resource_name>'\n location: '<region>'\n sku: { name: 'premium' }\n properties: {\n managedResourceGroupId: '/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>'\n parameters: {\n customVirtualNetworkId: { // CRITICAL: Enables VNet injection by attaching your VNet\n value: '/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>/providers/Microsoft.Network/virtualNetworks/<example_resource_name>'\n }\n customPublicSubnetName: { value: '<example_resource_name>-public' } // Required: host (public) subnet name\n customPrivateSubnetName: { value: '<example_resource_name>-private' } // Required: container (private) subnet name\n }\n }\n}\n```",
"Other": "1. In the Azure Portal, go to Create a resource > Azure Databricks\n2. On Basics, enter workspace name, region, and resource group\n3. Open the Networking tab and select Your VNet (VNet injection)\n4. Choose your Virtual network and select the Host (public) and Container (private) subnets\n5. Click Review + create, then Create\n6. Migrate workloads to this workspace and delete the non-VNet workspace if no longer needed",
"Terraform": "```hcl\n# Azure Databricks workspace with VNet injection\nresource \"azurerm_databricks_workspace\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<region>\"\n sku = \"premium\"\n\n custom_parameters {\n virtual_network_id = \"/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>/providers/Microsoft.Network/virtualNetworks/<example_resource_name>\" # CRITICAL: Enables VNet injection by using your VNet\n public_subnet_name = \"<example_resource_name>-public\" # Required: host (public) subnet\n private_subnet_name = \"<example_resource_name>-private\" # Required: container (private) subnet\n }\n}\n```"
},
"Recommendation": {
"Text": "Deploy Databricks workspaces into a customer-managed VNet to ensure better control over network security and compliance.",
"Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Databricks/check-for-vnet-injection.html"
"Text": "Deploy workspaces in a customer-managed VNet and apply **defense in depth**:\n- Enforce egress control with firewalls/NAT and UDRs\n- Prefer **private endpoints** to public access\n- Apply **least privilege** NSG rules and segregate subnets\n- Use DNS controls and monitoring to detect anomalies",
"Url": "https://hub.prowler.com/check/databricks_workspace_vnet_injection_enabled"
}
},
"Categories": [],
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "iam_custom_role_has_permissions_to_administer_resource_locks",
"CheckTitle": "Ensure an IAM custom role has permissions to administer resource locks",
"CheckTitle": "Custom role has permission to administer resource locks",
"CheckType": [],
"ServiceName": "iam",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureRole",
"Severity": "medium",
"ResourceType": "microsoft.authorization/roledefinitions",
"ResourceGroup": "IAM",
"Description": "Ensure a Custom Role is Assigned Permissions for Administering Resource Locks",
"Risk": "In Azure, resource locks are a way to prevent accidental deletion or modification of critical resources. These locks can be set at the resource group level or the individual resource level. Resource locks administration is a critical task that should be preformed from a custom role with the appropriate permissions. This ensures that only authorized users can administer resource locks.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources?tabs=json",
"Description": "**Azure custom RBAC roles** include the `Microsoft.Authorization/locks/*` action, indicating permission to administer **management locks** at subscription, resource group, or resource scope.",
"Risk": "Absent a scoped custom role for `Microsoft.Authorization/locks/*`, lock control falls to broad roles (e.g., Owner), weakening **least privilege**. Locks can be disabled or altered, enabling unauthorized changes or deletion, harming **integrity** and **availability**, and reducing **separation of duties** and accountability.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources?tabs=json",
"https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/resource-lock-custom-role.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/resource-lock-custom-role.html",
"Terraform": ""
"CLI": "az role definition create --role-definition '{\"Name\":\"<role_name>\",\"Description\":\"Custom role to administer resource locks\",\"IsCustom\":true,\"Actions\":[\"Microsoft.Authorization/locks/*\"],\"NotActions\":[],\"AssignableScopes\":[\"/subscriptions/<subscription_id>\"]}'",
"NativeIaC": "```bicep\n// Custom role that can administer resource locks\ntargetScope = 'subscription'\n\nresource roleDef 'Microsoft.Authorization/roleDefinitions@2022-04-01' = {\n name: guid(subscription().id, '<role_name>') // CRITICAL: use GUID for role definition name\n properties: {\n roleName: '<role_name>'\n description: 'Custom role to administer resource locks'\n permissions: [\n {\n actions: [\n 'Microsoft.Authorization/locks/*' // CRITICAL: grants lock administration to pass the check\n ]\n notActions: []\n }\n ]\n assignableScopes: [ subscription().id ]\n }\n}\n```",
"Other": "1. In the Azure portal, go to the target scope (Subscription or Resource group) and open Access control (IAM)\n2. Click Roles, find your custom role, and select Edit\n3. Go to Permissions > Add permissions\n4. Search for \"Microsoft.Authorization/locks\" and select Microsoft.Authorization/locks/*\n5. Click Add, then Review + save > Save",
"Terraform": "```hcl\n# Custom role with permission to administer resource locks\nresource \"azurerm_role_definition\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n scope = \"/subscriptions/<example_resource_id>\"\n\n permissions {\n actions = [\n \"Microsoft.Authorization/locks/*\" # CRITICAL: adds lock admin permission to pass the check\n ]\n }\n\n assignable_scopes = [\"/subscriptions/<example_resource_id>\"]\n}\n```"
},
"Recommendation": {
"Text": "Resouce locks are needed to prevent accidental deletion or modification of critical Azure resources. The administration of resource locks should be performed from a custom role with the appropriate permissions.",
"Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/resource-lock-custom-role.html"
"Text": "Define a **least-privilege custom role** restricted to `Microsoft.Authorization/locks/*` and assign it to a tightly controlled group at minimal scope. Apply **separation of duties**, use just-in-time elevation, audit lock changes, and avoid broad roles or pipeline identities managing locks. Layer with **defense-in-depth** controls.",
"Url": "https://hub.prowler.com/check/iam_custom_role_has_permissions_to_administer_resource_locks"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "iam_role_user_access_admin_restricted",
"CheckTitle": "Ensure 'User Access Administrator' role is restricted",
"CheckTitle": "Role assignment does not grant the User Access Administrator role",
"CheckType": [],
"ServiceName": "iam",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureIAMRoleassignment",
"ResourceType": "microsoft.authorization/roleassignments",
"ResourceGroup": "IAM",
"Description": "Checks for active assignments of the highly privileged 'User Access Administrator' role in Azure subscriptions.",
"Risk": "Persistent assignment of this role can lead to privilege escalation and unauthorized access, increasing the risk of security breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#user-access-administrator",
"Description": "**Azure subscription role assignments** granting **User Access Administrator** are identified to surface principals able to manage access (`Azure RBAC`) at that scope.",
"Risk": "Persistent `User Access Administrator` enables assigning high-privilege roles and reading control-plane data, enabling privilege escalation and unauthorized access. Impact: **confidentiality** (data exposure), **integrity** (unauthorized changes), **availability** (service disruption).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin?tabs=azure-portal%2Centra-audit-logs",
"https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#user-access-administrator"
],
"Remediation": {
"Code": {
"CLI": "az role assignment delete --role 'User Access Administrator' --scope '/subscriptions/<subscription_id>'",
"CLI": "az role assignment delete --assignee <principal_id_or_upn> --role \"User Access Administrator\" --scope \"/subscriptions/<subscription_id>\"",
"NativeIaC": "",
"Other": "",
"Other": "1. In the Azure portal, go to Subscriptions and select <subscription>.\n2. Open Access control (IAM) > Role assignments.\n3. Filter by Role = User Access Administrator.\n4. Select the assignment(s) and click Remove. Confirm.",
"Terraform": ""
},
"Recommendation": {
"Text": "Remove 'User Access Administrator' role assignments immediately after use to minimize security risks.",
"Url": "https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin?tabs=azure-portal%2Centra-audit-logs"
"Text": "Enforce **least privilege**:\n- Avoid standing `User Access Administrator`; use time-bound, approval-based elevation (PIM)\n- Scope access to only required subscriptions/resource groups\n- Require MFA and monitor role activity\n- Review regularly and remove unused grants",
"Url": "https://hub.prowler.com/check/iam_role_user_access_admin_restricted"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "iam_subscription_roles_owner_custom_not_created",
"CheckTitle": "Ensure that no custom subscription owner roles are created",
"CheckTitle": "Custom role is not a subscription owner role",
"CheckType": [],
"ServiceName": "iam",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureRole",
"ResourceType": "microsoft.authorization/roledefinitions",
"ResourceGroup": "IAM",
"Description": "Ensure that no custom subscription owner roles are created",
"Risk": "Subscription ownership should not include permission to create custom owner roles. The principle of least privilege should be followed and only necessary privileges should be assigned instead of allowing full administrative access.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/role-based-access-control/custom-roles",
"Description": "**Azure custom roles** are analyzed for wildcard permissions. Roles that allow `*` in `actions` within their assignable scopes are treated as **owner-equivalent**, granting unrestricted control over subscription resources.",
"Risk": "Wildcard access grants full administrative control at subscription scope. If abused or compromised, an actor can exfiltrate data, alter configurations, deploy malware, delete resources, and disable logging, impacting confidentiality, integrity, and availability across the subscription.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/role-based-access-control/custom-roles",
"https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html",
"Terraform": ""
"CLI": "az role definition update --role-definition '{\"Name\":\"<example_role_name>\",\"Description\":\"Restricted custom role\",\"Actions\":[\"Microsoft.Resources/subscriptions/resourceGroups/read\"],\"NotActions\":[],\"DataActions\":[],\"NotDataActions\":[],\"AssignableScopes\":[\"/subscriptions/<example_subscription_id>\"]}'",
"NativeIaC": "```bicep\n// Subscription-scoped deployment to ensure the custom role does not use global \"*\" permissions\ntargetScope = 'subscription'\n\nresource roleDef 'Microsoft.Authorization/roleDefinitions@2022-04-01' = {\n name: guid(subscription().id, '<role_name>') // CRITICAL: use GUID for role definition name\n properties: {\n roleName: '<role_name>'\n description: 'Restricted custom role'\n assignableScopes: [\n subscription().id\n ]\n permissions: [\n {\n actions: [\n 'Microsoft.Resources/subscriptions/resourceGroups/read' // CRITICAL: remove \"*\" and allow only specific actions to avoid owner-equivalent wildcard\n ]\n notActions: []\n dataActions: []\n notDataActions: []\n }\n ]\n }\n}\n```",
"Other": "1. In the Azure portal, go to Subscriptions > <your subscription> > Access control (IAM)\n2. Select the Roles tab, then open the Custom roles tab\n3. Click the custom role that is failing, then click Edit\n4. In Permissions, remove the action \"*\" (All permissions)\n5. Add only the specific actions required (avoid using \"*\")\n6. Click Save",
"Terraform": "```hcl\n# Define a custom role without using the global \"*\" action\nresource \"azurerm_role_definition\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n scope = \"/subscriptions/<example_subscription_id>\"\n\n permissions {\n actions = [\"Microsoft.Resources/subscriptions/resourceGroups/read\"] # CRITICAL: do not use \"*\"; specify only required actions\n }\n\n assignable_scopes = [\"/subscriptions/<example_subscription_id>\"]\n}\n```"
},
"Recommendation": {
"Text": "Custom subscription owner roles should not be created. This is because the principle of least privilege should be followed and only necessary privileges should be assigned instead of allowing full administrative access",
"Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html"
"Text": "Avoid owner-equivalent custom roles. Apply **least privilege**: prefer built-in roles, define explicit allowed `actions` (avoid `*`), and limit assignment scope to the minimum needed. Enforce **separation of duties**, require just-in-time elevation, and perform periodic access reviews to prevent privilege creep.",
"Url": "https://hub.prowler.com/check/iam_subscription_roles_owner_custom_not_created"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -5,22 +5,21 @@ from prowler.providers.azure.services.keyvault.keyvault_client import keyvault_c
class keyvault_rbac_secret_expiration_set(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for subscription, key_vaults in keyvault_client.key_vaults.items():
for keyvault in key_vaults:
if keyvault.properties.enable_rbac_authorization and keyvault.secrets:
report = Check_Report_Azure(
metadata=self.metadata(), resource=keyvault
)
report.subscription = subscription
report.status = "PASS"
report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has all the secrets with expiration date set."
has_secret_without_expiration = False
for secret in keyvault.secrets:
report = Check_Report_Azure(
metadata=self.metadata(), resource=secret
)
report.subscription = subscription
if not secret.attributes.expires and secret.enabled:
report.status = "FAIL"
report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has the secret {secret.name} without expiration date set."
has_secret_without_expiration = True
findings.append(report)
if not has_secret_without_expiration:
report.status_extended = f"Secret '{secret.name}' in KeyVault '{keyvault.name}' does not have expiration date set."
else:
report.status = "PASS"
report.status_extended = f"Secret '{secret.name}' in KeyVault '{keyvault.name}' has expiration date set."
findings.append(report)
return findings
@@ -1,3 +1,4 @@
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional, Union
@@ -20,99 +21,155 @@ class KeyVault(AzureService):
self.key_vaults = self._get_key_vaults(provider)
def _get_key_vaults(self, provider):
"""
Get all KeyVaults with parallel processing.
Optimizations:
1. Uses list_by_subscription() for full Vault objects
2. Processes vaults in parallel using __threading_call__
3. Each vault's keys/secrets/monitor fetched in parallel
"""
logger.info("KeyVault - Getting key_vaults...")
key_vaults = {}
for subscription, client in self.clients.items():
try:
key_vaults.update({subscription: []})
key_vaults_list = client.vaults.list()
for keyvault in key_vaults_list:
resource_group = keyvault.id.split("/")[4]
keyvault_name = keyvault.name
keyvault_properties = client.vaults.get(
resource_group, keyvault_name
).properties
keys = self._get_keys(
subscription, resource_group, keyvault_name, provider
)
secrets = self._get_secrets(
subscription, resource_group, keyvault_name
)
key_vaults[subscription].append(
KeyVaultInfo(
id=getattr(keyvault, "id", ""),
name=getattr(keyvault, "name", ""),
location=getattr(keyvault, "location", ""),
resource_group=resource_group,
properties=VaultProperties(
tenant_id=getattr(keyvault_properties, "tenant_id", ""),
enable_rbac_authorization=getattr(
keyvault_properties,
"enable_rbac_authorization",
False,
),
private_endpoint_connections=[
PrivateEndpointConnection(id=conn.id)
for conn in (
getattr(
keyvault_properties,
"private_endpoint_connections",
[],
)
or []
)
],
enable_soft_delete=getattr(
keyvault_properties, "enable_soft_delete", False
),
enable_purge_protection=getattr(
keyvault_properties,
"enable_purge_protection",
False,
),
public_network_access_disabled=(
getattr(
keyvault_properties,
"public_network_access",
"Enabled",
)
== "Disabled"
),
),
keys=keys,
secrets=secrets,
monitor_diagnostic_settings=self._get_vault_monitor_settings(
keyvault_name, resource_group, subscription
),
)
)
key_vaults[subscription] = []
vaults_list = list(client.vaults.list_by_subscription())
if not vaults_list:
continue
# Prepare items for parallel processing
items = [
{
"subscription": subscription,
"keyvault": vault,
"provider": provider,
}
for vault in vaults_list
]
# Process all KeyVaults in parallel
results = self.__threading_call__(self._process_single_keyvault, items)
key_vaults[subscription] = results
except Exception as error:
logger.error(
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return key_vaults
def _process_single_keyvault(self, item: dict) -> Optional["KeyVaultInfo"]:
"""Process a single KeyVault in parallel."""
subscription = item["subscription"]
keyvault = item["keyvault"]
provider = item["provider"]
try:
resource_group = keyvault.id.split("/")[4]
keyvault_name = keyvault.name
keyvault_properties = keyvault.properties
# Fetch keys, secrets, and monitor in parallel
with ThreadPoolExecutor(max_workers=3) as executor:
keys_future = executor.submit(
self._get_keys,
subscription,
resource_group,
keyvault_name,
provider,
)
secrets_future = executor.submit(
self._get_secrets, subscription, resource_group, keyvault_name
)
monitor_future = executor.submit(
self._get_vault_monitor_settings,
keyvault_name,
resource_group,
subscription,
)
keys = keys_future.result()
secrets = secrets_future.result()
monitor_settings = monitor_future.result()
return KeyVaultInfo(
id=getattr(keyvault, "id", ""),
name=getattr(keyvault, "name", ""),
location=getattr(keyvault, "location", ""),
resource_group=resource_group,
properties=VaultProperties(
tenant_id=getattr(keyvault_properties, "tenant_id", ""),
enable_rbac_authorization=getattr(
keyvault_properties,
"enable_rbac_authorization",
False,
),
private_endpoint_connections=[
PrivateEndpointConnection(id=conn.id)
for conn in (
getattr(
keyvault_properties,
"private_endpoint_connections",
[],
)
or []
)
],
enable_soft_delete=getattr(
keyvault_properties, "enable_soft_delete", False
),
enable_purge_protection=getattr(
keyvault_properties,
"enable_purge_protection",
False,
),
public_network_access_disabled=(
getattr(
keyvault_properties,
"public_network_access",
"Enabled",
)
== "Disabled"
),
),
keys=keys,
secrets=secrets,
monitor_diagnostic_settings=monitor_settings,
)
except Exception as error:
logger.error(
f"KeyVault {keyvault.name} in {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return None
def _get_keys(self, subscription, resource_group, keyvault_name, provider):
logger.info(f"KeyVault - Getting keys for {keyvault_name}...")
keys = []
keys_dict = {}
try:
client = self.clients[subscription]
keys_list = client.keys.list(resource_group, keyvault_name)
for key in keys_list:
keys.append(
Key(
id=getattr(key, "id", ""),
name=getattr(key, "name", ""),
key_obj = Key(
id=getattr(key, "id", ""),
name=getattr(key, "name", ""),
enabled=getattr(key.attributes, "enabled", False),
location=getattr(key, "location", ""),
attributes=KeyAttributes(
enabled=getattr(key.attributes, "enabled", False),
location=getattr(key, "location", ""),
attributes=KeyAttributes(
enabled=getattr(key.attributes, "enabled", False),
created=getattr(key.attributes, "created", 0),
updated=getattr(key.attributes, "updated", 0),
expires=getattr(key.attributes, "expires", 0),
),
)
created=getattr(key.attributes, "created", 0),
updated=getattr(key.attributes, "updated", 0),
expires=getattr(key.attributes, "expires", 0),
),
)
keys.append(key_obj)
keys_dict[key_obj.name] = key_obj
except Exception as error:
logger.error(
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -124,12 +181,19 @@ class KeyVault(AzureService):
# TODO: review the following line
credential=provider.session,
)
properties = key_client.list_properties_of_keys()
for prop in properties:
policy = key_client.get_key_rotation_policy(prop.name)
for key in keys:
if key.name == prop.name:
key.rotation_policy = KeyRotationPolicy(
properties = list(key_client.list_properties_of_keys())
if properties:
items = [
{"key_client": key_client, "prop": prop} for prop in properties
]
rotation_results = self.__threading_call__(
self._get_single_rotation_policy, items
)
for name, policy in rotation_results:
if policy and name in keys_dict:
keys_dict[name].rotation_policy = KeyRotationPolicy(
id=getattr(policy, "id", ""),
lifetime_actions=[
KeyRotationLifetimeAction(action=action.action)
@@ -142,8 +206,25 @@ class KeyVault(AzureService):
logger.warning(
f"Subscription name: {subscription} -- has no access policy configured for keyvault {keyvault_name}"
)
return keys
def _get_single_rotation_policy(self, item: dict) -> tuple:
"""Thread-safe rotation policy retrieval."""
key_client = item["key_client"]
prop = item["prop"]
try:
policy = key_client.get_key_rotation_policy(prop.name)
return (prop.name, policy)
except HttpResponseError:
return (prop.name, None)
except Exception as error:
logger.warning(
f"KeyVault - Failed to get rotation policy for key {prop.name}: {error}"
)
return (prop.name, None)
def _get_secrets(self, subscription, resource_group, keyvault_name):
logger.info(f"KeyVault - Getting secrets for {keyvault_name}...")
secrets = []
@@ -177,6 +258,7 @@ class KeyVault(AzureService):
logger.error(
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return secrets
def _get_vault_monitor_settings(self, keyvault_name, resource_group, subscription):
@@ -192,8 +274,9 @@ class KeyVault(AzureService):
)
except Exception as error:
logger.error(
f"Subscription name: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return monitor_diagnostics_settings
@@ -1,27 +1,32 @@
{
"Provider": "azure",
"CheckID": "policy_ensure_asc_enforcement_enabled",
"CheckTitle": "Ensure Any of the ASC Default Policy Settings are Not Set to 'Disabled'",
"CheckTitle": "Security Center built-in policy assignment has enforcement mode set to Default",
"CheckType": [],
"ServiceName": "policy",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Authorization/policyAssignments",
"ResourceType": "microsoft.authorization/policyassignments",
"ResourceGroup": "governance",
"Description": "None of the settings offered by ASC Default policy should be set to effect Disabled.",
"Risk": "A security policy defines the desired configuration of your workloads and helps ensure compliance with company or regulatory security requirements. ASC Default policy is associated with every subscription by default. ASC default policy assignment is a set of security recommendations based on best practices. Enabling recommendations in ASC default policy ensures that Azure security center provides the ability to monitor all of the supported recommendations and optionally allow automated action for a few of the supported recommendations.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/security-policy-concept",
"Description": "**Defender for Cloud default policy assignment** (`SecurityCenterBuiltIn`) uses enforcement mode `Default` rather than `DoNotEnforce`",
"Risk": "With `DoNotEnforce`, policy effects like `deny` and `deployIfNotExists` aren't applied, letting insecure configs persist. This erodes **confidentiality** and **integrity** (exposed endpoints, weak encryption) and can affect **availability** via unpatched or misconfigured services, enabling compromise and lateral movement.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/policy-reference",
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/implement-security-recommendations",
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/security-policy-concept"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az policy assignment update --name SecurityCenterBuiltIn --scope /subscriptions/<SUBSCRIPTION_ID> --enforcement-mode Default",
"NativeIaC": "```bicep\n// Set enforcement mode to Default for the Security Center built-in assignment\n// Deploy at subscription scope\ntargetScope = 'subscription'\n\nresource policyAssignment 'Microsoft.Authorization/policyAssignments@2021-06-01' = {\n name: 'SecurityCenterBuiltIn'\n properties: {\n policyDefinitionId: '<POLICY_OR_INITIATIVE_DEFINITION_ID>'\n enforcementMode: 'Default' // CRITICAL: Ensures the assignment enforces policy (fixes the finding)\n }\n}\n```",
"Other": "1. In Azure portal, go to Policy > Assignments\n2. Find the assignment named \"SecurityCenterBuiltIn\" and select it\n3. Click Edit assignment\n4. Set Enforcement mode to Enabled (Default)\n5. Click Review + save to apply",
"Terraform": "```hcl\n# Set enforcement mode to Default for the Security Center built-in assignment\nresource \"azurerm_policy_assignment\" \"<example_resource_name>\" {\n name = \"SecurityCenterBuiltIn\"\n scope = \"/subscriptions/<SUBSCRIPTION_ID>\"\n policy_definition_id = \"<POLICY_OR_INITIATIVE_DEFINITION_ID>\"\n enforcement_mode = \"Default\" # CRITICAL: Enables enforcement to pass the check\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Policy 3. Select ASC Default for each subscription 4. Click on 'view Assignment' 5. Click on 'Edit assignment' 6. Ensure Policy Enforcement is Enabled 7. Click 'Review + Save'",
"Url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/implement-security-recommendations"
"Text": "Keep enforcement mode `Default` on the default initiative and avoid disabling critical effects. Apply at scale for consistent governance, align with **least privilege** and **defense in depth**, validate changes in `Audit` in non-prod, and manage justified exceptions via time-bound policy exemptions instead of turning enforcement off.",
"Url": "https://hub.prowler.com/check/policy_ensure_asc_enforcement_enabled"
}
},
"Categories": [],
@@ -0,0 +1,39 @@
{
"Provider": "gcp",
"CheckID": "compute_instance_on_host_maintenance_migrate",
"CheckTitle": "Compute Engine VM instance has On Host Maintenance set to MIGRATE",
"CheckType": [],
"ServiceName": "compute",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "compute.googleapis.com/Instance",
"ResourceGroup": "compute",
"Description": "**Compute Engine VM instances** should have their **On Host Maintenance** setting configured to `MIGRATE` for live migration during host maintenance events, ensuring continuous availability without downtime.",
"Risk": "VM instances configured with On Host Maintenance set to `TERMINATE` will be shut down during host maintenance events, causing **service interruptions** and **unplanned downtime**. This can impact application availability and may require manual intervention to restart services.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/configure-maintenance-behavior.html",
"https://cloud.google.com/compute/docs/instances/setting-instance-scheduling-options"
],
"Remediation": {
"Code": {
"CLI": "gcloud compute instances set-scheduling <INSTANCE_NAME> --maintenance-policy=MIGRATE --zone=<ZONE>",
"NativeIaC": "",
"Other": "1. Open Google Cloud Console and navigate to Compute Engine > VM instances\n2. Click on the instance name to view details\n3. Click 'Edit' at the top of the page\n4. Under 'Availability policies', set 'On host maintenance' to 'Migrate VM instance (recommended)'\n5. Click 'Save' at the bottom of the page",
"Terraform": "```hcl\n# Example: configure On Host Maintenance to MIGRATE for a Compute Engine VM instance\nresource \"google_compute_instance\" \"example\" {\n name = var.instance_name\n machine_type = var.machine_type\n zone = var.zone\n\n scheduling {\n # Live migrate during host maintenance events\n on_host_maintenance = \"MIGRATE\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Configure VM instances to use **live migration** during host maintenance events to ensure continuous availability. This is the recommended setting for production workloads that require high availability.",
"Url": "https://hub.prowler.com/check/compute_instance_on_host_maintenance_migrate"
}
},
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [
"compute_instance_automatic_restart_enabled"
],
"Notes": "Preemptible and Spot VMs cannot use MIGRATE and will always be TERMINATE. The default value for this setting is MIGRATE."
}
@@ -0,0 +1,41 @@
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.compute.compute_client import compute_client
class compute_instance_on_host_maintenance_migrate(Check):
"""
Ensure Compute Engine VM instances have On Host Maintenance set to MIGRATE.
This check evaluates whether VM instances are configured to live migrate during
host maintenance events, preventing downtime when Google performs maintenance.
- PASS: VM instance has On Host Maintenance set to MIGRATE.
- FAIL: VM instance has On Host Maintenance set to TERMINATE.
"""
def execute(self) -> list[Check_Report_GCP]:
findings = []
for instance in compute_client.instances:
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
if instance.on_host_maintenance == "MIGRATE":
report.status = "PASS"
report.status_extended = f"VM Instance {instance.name} has On Host Maintenance set to MIGRATE."
else:
report.status = "FAIL"
if instance.preemptible or instance.provisioning_model == "SPOT":
vm_type = "preemptible" if instance.preemptible else "Spot"
report.status_extended = (
f"VM Instance {instance.name} is a {vm_type} VM and has On Host Maintenance set to TERMINATE. "
f"{vm_type.capitalize()} VMs cannot use MIGRATE and must always use TERMINATE. "
f"If high availability is required, consider using a non-preemptible VM instead."
)
else:
report.status_extended = (
f"VM Instance {instance.name} has On Host Maintenance set to "
f"{instance.on_host_maintenance} instead of MIGRATE."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "gcp",
"CheckID": "compute_instance_suspended_without_persistent_disks",
"CheckTitle": "Suspended VM instance does not have persistent disks attached",
"CheckType": [],
"ServiceName": "compute",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "compute.googleapis.com/Instance",
"ResourceGroup": "compute",
"Description": "This check identifies VM instances in a **SUSPENDED** or **SUSPENDING** state with persistent disks still attached.\n\nPersistent disks on suspended VMs remain accessible through the GCP API and could contain **sensitive data** while the instance is inactive, potentially creating security blind spots in long-forgotten infrastructure.",
"Risk": "Persistent disks on suspended VM instances remain accessible through the GCP API and may contain **sensitive data**, creating potential security risks:\n\n- **Unauthorized data access** if credentials are compromised or permissions are misconfigured\n- **Data exposure** from forgotten infrastructure that is no longer actively monitored\n- **Security blind spots** where suspended resources are overlooked during security reviews and audits",
"RelatedUrl": "",
"AdditionalURLs": [
"https://cloud.google.com/icompute/docs/instances/suspend-resume-instance",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/persistent-disks-attached-to-suspended-vms.html"
],
"Remediation": {
"Code": {
"CLI": "gcloud compute instances delete INSTANCE_NAME --zone=ZONE",
"NativeIaC": "",
"Other": "1. Open the Google Cloud Console\n2. Navigate to Compute Engine > VM instances\n3. Identify suspended instances with attached disks\n4. If the instance is no longer needed, select it and click DELETE\n5. If the instance will be resumed, take no action or resume it with: gcloud compute instances resume INSTANCE_NAME --zone=ZONE",
"Terraform": "```hcl\n# To remediate, either delete the suspended instance or resume it\n# Delete by removing the resource from your Terraform configuration\n# Or resume by changing the desired_status\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n # Set desired_status to RUNNING to resume the instance\n desired_status = \"RUNNING\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Regularly review suspended VM instances to reduce your attack surface. Either **resume** instances if still needed, or **delete** them along with their attached disks to eliminate potential data exposure. Implement automated policies to detect and alert on long-suspended instances as part of your security monitoring.",
"Url": "https://hub.prowler.com/check/compute_instance_suspended_without_persistent_disks"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [
"compute_instance_disk_auto_delete_disabled"
],
"Notes": "This check is focused on security risks rather than cost optimization. Persistent disks on suspended VMs remain accessible and may contain sensitive data, creating potential unauthorized access risks."
}
@@ -0,0 +1,35 @@
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.compute.compute_client import compute_client
class compute_instance_suspended_without_persistent_disks(Check):
"""
Ensure that VM instances in SUSPENDED state do not have persistent disks attached.
This check identifies VM instances that are in a SUSPENDED or SUSPENDING state
and have persistent disks still attached. Suspended VMs with attached disks
represent unused infrastructure that continues to incur storage costs.
- PASS: VM instance is not in SUSPENDED/SUSPENDING state, or is suspended but has no disks attached.
- FAIL: VM instance is in SUSPENDED/SUSPENDING state with persistent disks attached.
"""
def execute(self) -> list[Check_Report_GCP]:
findings = []
for instance in compute_client.instances:
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
report.status = "PASS"
report.status_extended = f"VM Instance {instance.name} is not suspended."
if instance.status in ("SUSPENDED", "SUSPENDING"):
attached_disks = [disk.name for disk in instance.disks]
if attached_disks:
report.status = "FAIL"
report.status_extended = f"VM Instance {instance.name} is {instance.status.lower()} with {len(attached_disks)} persistent disk(s) attached: {', '.join(attached_disks)}."
else:
report.status_extended = f"VM Instance {instance.name} is {instance.status.lower()} but has no persistent disks attached."
findings.append(report)
return findings
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more