mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-02 09:24:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1bb376e66 | ||
|
|
91b86777ce |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "prowler-plugins",
|
||||
"description": "Prowler Cloud security and compliance for Claude",
|
||||
"description": "Prowler Cloud Security for Claude Code",
|
||||
"owner": {
|
||||
"name": "Prowler",
|
||||
"email": "support@prowler.com"
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "prowler",
|
||||
"source": "./claude_plugins/prowler",
|
||||
"description": "Connect Claude to Prowler Cloud through the Prowler MCP server. Reads providers, scans, findings, compliance results and resources for AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and the other providers listed at https://registry.prowler.com, and returns remediation guidance for failed checks. Writes are limited to Prowler Cloud itself: triggering scans, finding triage, mute rules and the mutelist, providers and integrations. Your cloud resources are not changed by the connector.",
|
||||
"description": "Prowler Cloud security and compliance skills for Claude, powered by the Prowler MCP server. Triage compliance gaps and remediate findings on AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and many more providers listed at https://registry.prowler.com, plus custom providers, until your account meets the framework you choose.",
|
||||
"category": "security",
|
||||
"homepage": "https://prowler.com"
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ SENTRY_RELEASE=local
|
||||
# REO_DEV_CLIENT_ID=
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.48.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.46.1
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -139,6 +139,10 @@ jobs:
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pnpm run healthcheck
|
||||
|
||||
- name: Check product-tour alignment
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pnpm run tour:check
|
||||
|
||||
- name: Run pnpm audit
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pnpm run audit
|
||||
|
||||
@@ -95,10 +95,3 @@ ignore:
|
||||
package:
|
||||
name: zlib
|
||||
version: 1.3.2-r0
|
||||
|
||||
# Documented in .trivyignore.yaml: go-jose 4.1.5 compiled into the Trivy binary (0.76.1,
|
||||
# the latest release). Pinned to 4.1.5 so it stops matching with the next Trivy bump.
|
||||
- vulnerability: CVE-2026-116587
|
||||
package:
|
||||
name: github.com/go-jose/go-jose/v4
|
||||
version: v4.1.5
|
||||
@@ -226,11 +226,3 @@ vulnerabilities:
|
||||
purls:
|
||||
- "pkg:nuget/System.Security.Cryptography.Pkcs@10.0.0"
|
||||
expired_at: 2026-12-15
|
||||
|
||||
# go-jose 4.1.5 is compiled into the Trivy binary (usr/local/bin/trivy). Trivy merged the
|
||||
# 4.1.6 bump upstream (aquasecurity/trivy#11488) but 0.76.1 is still the latest release.
|
||||
# Bump TRIVY_VERSION and drop this entry once a release ships it.
|
||||
- id: CVE-2026-116587
|
||||
purls:
|
||||
- "pkg:golang/github.com/go-jose/go-jose/v4@v4.1.5"
|
||||
expired_at: 2026-11-20
|
||||
@@ -51,6 +51,7 @@ Use these skills for detailed patterns on-demand:
|
||||
| `django-migration-psql` | Django migration best practices for PostgreSQL | [SKILL.md](skills/django-migration-psql/SKILL.md) |
|
||||
| `postgresql-indexing` | PostgreSQL indexing, EXPLAIN, monitoring, maintenance | [SKILL.md](skills/postgresql-indexing/SKILL.md) |
|
||||
| `prowler-attack-paths-query` | Create Attack Paths openCypher queries | [SKILL.md](skills/prowler-attack-paths-query/SKILL.md) |
|
||||
| `prowler-tour` | Keep product-tour definitions aligned with the UI | [SKILL.md](skills/prowler-tour/SKILL.md) |
|
||||
| `gh-aw` | GitHub Agentic Workflows (gh-aw) | [SKILL.md](skills/gh-aw/SKILL.md) |
|
||||
| `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) |
|
||||
|
||||
@@ -68,10 +69,12 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
| Adding new providers | `prowler-provider` |
|
||||
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
|
||||
| Adding services to existing providers | `prowler-provider` |
|
||||
| Adding, updating, or removing a tour definition (*.tour.ts) | `prowler-tour` |
|
||||
| After creating/modifying a skill | `skill-sync` |
|
||||
| App Router / Server Actions | `nextjs-16` |
|
||||
| Auditing check-to-requirement mappings as a cloud auditor | `prowler-compliance` |
|
||||
| Building AI chat features | `ai-sdk-5` |
|
||||
| Changing button labels or section headings on a tour-covered page | `prowler-tour` |
|
||||
| Committing changes | `prowler-commit` |
|
||||
| Configuring MCP servers in agentic workflows | `gh-aw` |
|
||||
| Create PR that requires changelog entry | `prowler-changelog` |
|
||||
@@ -91,6 +94,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
| Creating/updating compliance frameworks | `prowler-compliance` |
|
||||
| Debug why a GitHub Actions job is failing | `prowler-ci` |
|
||||
| Debugging gh-aw compilation errors | `gh-aw` |
|
||||
| Editing a UI file containing data-tour-id attributes | `prowler-tour` |
|
||||
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
|
||||
| Fixing bug | `tdd` |
|
||||
| Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs) | `prowler-compliance` |
|
||||
@@ -107,6 +111,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
| Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` |
|
||||
| Refactoring code | `tdd` |
|
||||
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
|
||||
| Renaming or removing a data-tour-id attribute value | `prowler-tour` |
|
||||
| Restructuring routes or layouts covered by a tour | `prowler-tour` |
|
||||
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
|
||||
| Review changelog format and conventions | `prowler-changelog` |
|
||||
| Reviewing JSON:API compliance | `jsonapi` |
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.16-slim-trixie@sha256:8523d6abeb48ceae0b7240d30c5808fc26cb79c1a635bacdf19cf3baecfbec07 AS build
|
||||
FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
@@ -4,14 +4,6 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.48.0] (Prowler v5.47.0)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- pymongo to 4.18.3, dnspython to 2.9.0 and oauthlib to 4.0.0 in the API container image, with django-allauth from 65.15.0 to 65.19.7 because earlier releases cap oauthlib below 4, patching the BSON decoding and DNS compression pointer denial of service advisories and the OAuth device flow token disclosure [(#13185)](https://github.com/prowler-cloud/prowler/pull/13185)
|
||||
|
||||
---
|
||||
|
||||
## [1.47.0] (Prowler v5.46.0)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.16-slim-trixie@sha256:8523d6abeb48ceae0b7240d30c5808fc26cb79c1a635bacdf19cf3baecfbec07 AS build
|
||||
FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/api"
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Python base image from 3.12.15 to 3.12.16 in the API container image, patching CVE-2026-115557
|
||||
+7
-7
@@ -26,7 +26,7 @@ dependencies = [
|
||||
"celery (==5.6.2)",
|
||||
"dj-rest-auth[with_social,jwt] (==7.0.1)",
|
||||
"django (==5.2.17)",
|
||||
"django-allauth[saml] (==65.19.7)",
|
||||
"django-allauth[saml] (==65.15.0)",
|
||||
"django-celery-beat (==2.9.0)",
|
||||
"django-celery-results (==2.6.0)",
|
||||
"django-cors-headers==4.4.0",
|
||||
@@ -45,7 +45,7 @@ dependencies = [
|
||||
"gunicorn==26.0.0",
|
||||
"uvloop==0.22.1",
|
||||
"lxml==6.1.0",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.46",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (==1.3.0)",
|
||||
"sentry-sdk[django] (==2.56.0)",
|
||||
@@ -71,7 +71,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.49.0"
|
||||
version = "1.47.1"
|
||||
|
||||
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
|
||||
# target-version tracks this project's lowest supported Python.
|
||||
@@ -223,7 +223,7 @@ constraint-dependencies = [
|
||||
"distro==1.9.0",
|
||||
"dj-rest-auth==7.0.1",
|
||||
"django==5.2.17",
|
||||
"django-allauth==65.19.7",
|
||||
"django-allauth==65.15.0",
|
||||
"django-celery-beat==2.9.0",
|
||||
"django-celery-results==2.6.0",
|
||||
"django-cors-headers==4.4.0",
|
||||
@@ -237,7 +237,7 @@ constraint-dependencies = [
|
||||
"djangorestframework==3.18.3",
|
||||
"djangorestframework-jsonapi==7.0.2",
|
||||
"djangorestframework-simplejwt==5.5.1",
|
||||
"dnspython==2.9.0",
|
||||
"dnspython==2.8.0",
|
||||
"docker==7.1.0",
|
||||
"dogpile-cache==1.5.0",
|
||||
"dparse==0.6.4",
|
||||
@@ -337,7 +337,7 @@ constraint-dependencies = [
|
||||
"nest-asyncio==1.6.0",
|
||||
"nltk==3.9.4",
|
||||
"numpy==2.2.6",
|
||||
"oauthlib==4.0.0",
|
||||
"oauthlib==3.3.1",
|
||||
"oci==2.184.1",
|
||||
"openai==1.109.1",
|
||||
"openstacksdk==4.2.0",
|
||||
@@ -485,7 +485,7 @@ override-dependencies = [
|
||||
"okta==3.4.2",
|
||||
# huaweicloudsdkcore (via prowler) pins pymongo==4.15.1 exactly; CVE-2026-96748 and
|
||||
# CVE-2026-96749 are fixed in 4.18.2. Only bson types are used, so the override is safe.
|
||||
"pymongo==4.18.3",
|
||||
"pymongo==4.18.2",
|
||||
# prowler requires cryptography==50.0.0. Two api-only dependencies still cap it below
|
||||
# 49 and cannot move yet: msal, pinned exactly by azure-cli-core (2.83.0 -> 1.35.0b1,
|
||||
# 2.89.1 -> 1.36.0, both <49; cartography needs azure-cli-core), and workos 8.3.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.49.0
|
||||
version: 1.47.1
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
Generated
+67
-40
@@ -139,7 +139,7 @@ constraints = [
|
||||
{ name = "distro", specifier = "==1.9.0" },
|
||||
{ name = "dj-rest-auth", specifier = "==7.0.1" },
|
||||
{ name = "django", specifier = "==5.2.17" },
|
||||
{ name = "django-allauth", specifier = "==65.19.7" },
|
||||
{ name = "django-allauth", specifier = "==65.15.0" },
|
||||
{ name = "django-celery-beat", specifier = "==2.9.0" },
|
||||
{ name = "django-celery-results", specifier = "==2.6.0" },
|
||||
{ name = "django-cors-headers", specifier = "==4.4.0" },
|
||||
@@ -153,7 +153,7 @@ constraints = [
|
||||
{ name = "djangorestframework", specifier = "==3.18.3" },
|
||||
{ name = "djangorestframework-jsonapi", specifier = "==7.0.2" },
|
||||
{ name = "djangorestframework-simplejwt", specifier = "==5.5.1" },
|
||||
{ name = "dnspython", specifier = "==2.9.0" },
|
||||
{ name = "dnspython", specifier = "==2.8.0" },
|
||||
{ name = "docker", specifier = "==7.1.0" },
|
||||
{ name = "dogpile-cache", specifier = "==1.5.0" },
|
||||
{ name = "dparse", specifier = "==0.6.4" },
|
||||
@@ -253,7 +253,7 @@ constraints = [
|
||||
{ name = "nest-asyncio", specifier = "==1.6.0" },
|
||||
{ name = "nltk", specifier = "==3.9.4" },
|
||||
{ name = "numpy", specifier = "==2.2.6" },
|
||||
{ name = "oauthlib", specifier = "==4.0.0" },
|
||||
{ name = "oauthlib", specifier = "==3.3.1" },
|
||||
{ name = "oci", specifier = "==2.184.1" },
|
||||
{ name = "openai", specifier = "==1.109.1" },
|
||||
{ name = "openstacksdk", specifier = "==4.2.0" },
|
||||
@@ -388,7 +388,7 @@ overrides = [
|
||||
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" },
|
||||
{ name = "okta", specifier = "==3.4.2" },
|
||||
{ name = "pyjwt", extras = ["crypto"], specifier = "==2.14.0" },
|
||||
{ name = "pymongo", specifier = "==4.18.3" },
|
||||
{ name = "pymongo", specifier = "==4.18.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1229,6 +1229,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b3/8ec1268082f4d20cc8bf723a1a8e6b9e330bcc338a4dbcee9c7737e9dc1c/azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a", size = 1072620, upload-time = "2023-07-25T04:47:49.26Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-cognitiveservices"
|
||||
version = "14.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-mgmt-core" },
|
||||
{ name = "msrest" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/8e/9fcfdd507413a2536c5458b40942705ea8d74fe4e17f05fd1c32bb0225a9/azure_mgmt_cognitiveservices-14.1.0.tar.gz", hash = "sha256:915191374d0adb443863c20aaea2c9f3b9d558a849ac2b78f152249262fdcaf8", size = 184444, upload-time = "2025-10-24T07:28:14.229Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/25/ed6bbc01a03991255dd22aa41091360d225f1be20c88710af0f4a6c27459/azure_mgmt_cognitiveservices-14.1.0-py3-none-any.whl", hash = "sha256:3c0eecc00d183842a11d754f4f5c3328ff86f2d6c0a2b81b28dd7e073680258e", size = 290096, upload-time = "2025-10-24T07:28:15.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-mgmt-compute"
|
||||
version = "34.0.0"
|
||||
@@ -2345,15 +2359,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-allauth"
|
||||
version = "65.19.7"
|
||||
version = "65.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/33/7c8f8fc85f4ee644ee3acb20c1fe4d04516298ab5903f812e1556b65f947/django_allauth-65.19.7.tar.gz", hash = "sha256:c7749551b659ca954e483f6f634cd0c262d65dd8144f5219b3a31cba0426e981", size = 2296178, upload-time = "2026-10-01T12:35:24.152Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/c1/d3385f4c3169c1d6eea3c63aed0f36af51478c1d72e46db12bb1a08f8034/django_allauth-65.15.0.tar.gz", hash = "sha256:b404d48cf0c3ee14dacc834c541f30adedba2ff1c433980ecc494d6cb0b395a8", size = 2215709, upload-time = "2026-03-09T13:51:28.675Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/c4/a42cc4439b18b8d2767f8c54999e011b9f620a478806ed265baed7c3dc01/django_allauth-65.19.7-py3-none-any.whl", hash = "sha256:8899377f38afabf10445c9ee2f808a730304ca8aecc540657d51a8b3928283d6", size = 2076429, upload-time = "2026-10-01T12:35:20.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b8/c8411339171bd8bc075c09ef190fb42195e9a2149e5c5026e094fe62fce0/django_allauth-65.15.0-py3-none-any.whl", hash = "sha256:ad9fc49c49a9368eaa5bb95456b76e2a4f377b3c6862ee8443507816578c098d", size = 2022994, upload-time = "2026-03-09T13:51:19.711Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2550,11 +2564,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.9.0"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3503,6 +3517,17 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/7d/721f162c46e3de604a73674223bf6c6bc6cf7ade25b3751a71288f4dd122/huaweicloudsdkrds-3.1.204-py3-none-any.whl", hash = "sha256:a790b5b3c457a608e5679c101f463b4d037dd9a8a66f6e46144a9e5a4b37780f", size = 1626906, upload-time = "2026-07-09T09:04:06.936Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "huaweicloudsdksmn"
|
||||
version = "3.1.204"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huaweicloudsdkcore" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5d/c0de47d011f1932c9c0ddf669c238d9fad01653d438d38203703526799d7/huaweicloudsdksmn-3.1.204-py3-none-any.whl", hash = "sha256:b0818ea9293e27458c8fa1d2da021c98006cbf9ce01cf17a0f1df598c4da3a6c", size = 323674, upload-time = "2026-07-09T09:04:24.804Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "huaweicloudsdkvpc"
|
||||
version = "3.1.204"
|
||||
@@ -4419,11 +4444,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "oauthlib"
|
||||
version = "4.0.0"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/a1bcc8ba112a627f8ffbdc212a78ce18d3ac07e91a5ca65d27918eee25a1/oauthlib-4.0.0.tar.gz", hash = "sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86", size = 187232, upload-time = "2026-09-28T06:01:18.77Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/f4/78229a1066068ca14fc60fb26cf7381cabe4382261392b90e5f9552722d4/oauthlib-4.0.0-py3-none-any.whl", hash = "sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd", size = 159715, upload-time = "2026-09-28T06:01:17.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4842,8 +4867,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.41.0"
|
||||
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#f05a490cd74a2c0f11a5d66d8ce29d03fa5c64a2" }
|
||||
version = "5.46.0"
|
||||
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.46#fdb1bd619345b6f92bd6a7f8906fa5e6684a90a4" }
|
||||
dependencies = [
|
||||
{ name = "alibabacloud-actiontrail20200706" },
|
||||
{ name = "alibabacloud-credentials" },
|
||||
@@ -4864,6 +4889,7 @@ dependencies = [
|
||||
{ name = "azure-mgmt-apimanagement" },
|
||||
{ name = "azure-mgmt-applicationinsights" },
|
||||
{ name = "azure-mgmt-authorization" },
|
||||
{ name = "azure-mgmt-cognitiveservices" },
|
||||
{ name = "azure-mgmt-compute" },
|
||||
{ name = "azure-mgmt-containerregistry" },
|
||||
{ name = "azure-mgmt-containerservice" },
|
||||
@@ -4907,6 +4933,7 @@ dependencies = [
|
||||
{ name = "huaweicloudsdkkms" },
|
||||
{ name = "huaweicloudsdkobs" },
|
||||
{ name = "huaweicloudsdkrds" },
|
||||
{ name = "huaweicloudsdksmn" },
|
||||
{ name = "huaweicloudsdkvpc" },
|
||||
{ name = "huaweicloudsdkwaf" },
|
||||
{ name = "jsonschema" },
|
||||
@@ -4945,7 +4972,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.49.0"
|
||||
version = "1.47.1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
@@ -5020,7 +5047,7 @@ requires-dist = [
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
{ name = "dj-rest-auth", extras = ["with-social", "jwt"], specifier = "==7.0.1" },
|
||||
{ name = "django", specifier = "==5.2.17" },
|
||||
{ name = "django-allauth", extras = ["saml"], specifier = "==65.19.7" },
|
||||
{ name = "django-allauth", extras = ["saml"], specifier = "==65.15.0" },
|
||||
{ name = "django-celery-beat", specifier = "==2.9.0" },
|
||||
{ name = "django-celery-results", specifier = "==2.6.0" },
|
||||
{ name = "django-cors-headers", specifier = "==4.4.0" },
|
||||
@@ -5045,7 +5072,7 @@ requires-dist = [
|
||||
{ name = "matplotlib", specifier = "==3.10.8" },
|
||||
{ name = "neo4j", specifier = "==6.1.0" },
|
||||
{ name = "openai", specifier = "==1.109.1" },
|
||||
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" },
|
||||
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.46" },
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.9" },
|
||||
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
|
||||
{ name = "reportlab", specifier = "==4.4.10" },
|
||||
@@ -5371,33 +5398,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pymongo"
|
||||
version = "4.18.3"
|
||||
version = "4.18.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "dnspython" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/d8/2421a5ae0d6dcdaad2a0fb75d4071eaede9f764e73b829c62b6185c3ee6b/pymongo-4.18.3.tar.gz", hash = "sha256:5dd6e659b6014288a1c53458929402a58f44a032e6f29bcef44e7477c5268e48", size = 2747872, upload-time = "2026-10-08T19:44:08.343Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/a6/63bdeb527d98998b8ea2c667eca00d48f22dd42af281e9a2d7090632d54d/pymongo-4.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f00cb357d7cc7f2798116e2377732a409c43a6dc882f0241eafed7ffed50655", size = 818805, upload-time = "2026-10-08T19:42:09.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/e9/35602972d9fa98b894d1e5feef4db2f5d275298430e99b54b39f01125efa/pymongo-4.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fe2ef9c6eb6b75689e10b20a3d8119da87302481b0a7029f9399b35142adfd8", size = 819303, upload-time = "2026-10-08T19:42:10.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/e1/468f2c69b32565c93a56623535f248b091cf52e83c3509fa4b791427889f/pymongo-4.18.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6090d4bed582c97e38fa818c0a2b7443f203cb28882900b433ff713465f158", size = 1023754, upload-time = "2026-10-08T19:42:12.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/24/8af75e8af2427a47cfcc996444df934990ac4890d855f2ec064409d8b94b/pymongo-4.18.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f9903d0a089317422f52bbc25f5827e6656f0c42c43ed7d799bd02748e79a1", size = 1031920, upload-time = "2026-10-08T19:42:14.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d0/96fa79fb7cb47e6f09e58ccbe1a726330b5d052f395b65d57e286849ed43/pymongo-4.18.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac9bf2304c2b092ccf04261ab0cddb7fd65df1cc1ae0fa57312b03396c00d28c", size = 1053859, upload-time = "2026-10-08T19:42:16.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/99/1b3f48bd3580c53e4a0e89bdc8cd8c15af94ca944f9de96582cb3ebfa5d0/pymongo-4.18.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f37095428af3042f6bb1ebe269fedcbb645d9e0642b274e1cff026d3979500b", size = 1047418, upload-time = "2026-10-08T19:42:18.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/1e/ab9148b15dcefd3d02852a65e4ac3a1df86248c227dd532dc53b41f33697/pymongo-4.18.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16ade5053ab6c712fd25d3f878e38441b169d607d1326d708844a131911d029f", size = 1033358, upload-time = "2026-10-08T19:42:19.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/93/bbbd0edfa33b10e7a86c864478dbb12163c946c142fd1e7b1bd0b6490353/pymongo-4.18.3-cp311-cp311-win32.whl", hash = "sha256:463c09e2cc208a65d35a1af3c613360cff6d58c8aef652273da07250bb214dba", size = 814425, upload-time = "2026-10-08T19:42:21.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/13/7515f91ed9e80968cc5dc9321ac97fafde07afb75123adb9e294f50c8a10/pymongo-4.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:1d7d0474012def6113c224b167aae661b926ac3b788219426830013ea25acd33", size = 821387, upload-time = "2026-10-08T19:42:23.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/59/f54d5ee7d95ec4ed1f31bff014a0f61caa3e888d7a89a0585f3eb4be164b/pymongo-4.18.3-cp311-cp311-win_arm64.whl", hash = "sha256:83dff65baa6f2423857598ffc371d7412fa4d2a07c618bdc8d5053ade65de664", size = 817697, upload-time = "2026-10-08T19:42:25.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/d5/4775a2891396ad125545e23b3024adae4bfac9553b70c924f1f372269dbf/pymongo-4.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ea78719dd05de3a919a52b94bec790c0d0cb7d07d2f7271711832664502a0782", size = 819374, upload-time = "2026-10-08T19:42:26.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0b/89ad56f43c3da6cbde100699f6b99528e78eba3c6740d8dad4ea2516aa45/pymongo-4.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6029d14761ba7243e6c5e464592013b519ad4dd3e4cfb75ddec39f4b5910711b", size = 819694, upload-time = "2026-10-08T19:42:28.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b4/b68ffc205441b0a6d36d6299e35e063a5d0d3264fd685428920e1f82b634/pymongo-4.18.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9536fb3820f721290f03ad07472ec2266d8f364f91de628679a7146c9c1dbe35", size = 1040136, upload-time = "2026-10-08T19:42:30.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/40/e779ff3d9165316c35a2f9742a42b9c3e3a678e9e2a9f6fe4128b7c551eb/pymongo-4.18.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e461bfca4861057929efa4215730b28b93b2adb4d07828d0b65475755bbf63f5", size = 1051430, upload-time = "2026-10-08T19:42:32.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/9b/443ee038a739cc65a75f2078c9ef725c1cb4881545d2e9d7941c46f64a6c/pymongo-4.18.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1fef248623ed5e7406902a68d49dc0b1db434f19489f8d2fc9fe512c3c08bb1", size = 1075464, upload-time = "2026-10-08T19:42:34.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/4b/d80518f675cd4c1215b770444bb83002454574dae0e69760af10703ed1e8/pymongo-4.18.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:213eaed8fc4f2b0f9c84323a229dea699e01e18b8fb39723f430123b6ee77813", size = 1069046, upload-time = "2026-10-08T19:42:36.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/77/f2e9648c62e423c3b9dab1e16491a6c33250487c819e5c75784b35d16047/pymongo-4.18.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa6f363ff648bf061335d2190dd580cbf465b1308a7e6acb992d128d6a16a3bd", size = 1050321, upload-time = "2026-10-08T19:42:38.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/e4/3236e3a87b29fc4c502ad7dd1521c20d4a29faf1db6fde6ae94d05c5ec27/pymongo-4.18.3-cp312-cp312-win32.whl", hash = "sha256:28ba8cae86ea02d7ffdf0eea81be69be80d35d6a4a3eba4dc436d3194341805a", size = 815300, upload-time = "2026-10-08T19:42:40.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/3fa9d86ba32386c02ea991f10875a6a066dd5e5d80790243be3c141b0e73/pymongo-4.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc8ccf72b76c99a6b9fd05f8b89fe4a693128c5cfdba70f70e5792a6a563f6b0", size = 822079, upload-time = "2026-10-08T19:42:42.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/50/65a7cefd3891b77994841992b2c5b59394667df64ef21377b8ac7ecdef47/pymongo-4.18.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a1f7c7dc1d554449a1695d897eb42b6080a2f1e9ccd81385dfa00204979c54d", size = 817935, upload-time = "2026-10-08T19:42:43.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/01/e8358c562ce4e3255035f16eed43a6f77669b0779f3aa918362c670e869f/pymongo-4.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257519b59d8509a950b21c4a626e5295b3ad4397c12ec47e6e93353c49236145", size = 818562, upload-time = "2026-09-24T16:58:20.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/18/48a9e7f44a35560284b36bba24622a959ec1dd940e7c4f7cd308cfdba756/pymongo-4.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845b890622e7a2f031140f9cb1c0f266c1baa876ea359f7e51d0c38112cb3c6e", size = 819081, upload-time = "2026-09-24T16:58:22.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/24/2d9c0bb5649f262257f507b03f84419ac1254e94894cd493bda87fb935e1/pymongo-4.18.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a746f7e183c6d48aba6152d9f333e3bae2e8b6b00fac7abe69d4bdb3d10915c8", size = 1023284, upload-time = "2026-09-24T16:58:23.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/40/af888435ce8e3ae1e1a201b87fb397aa91e3ce115bb1bb3999197f2b9b72/pymongo-4.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9d13d812e55d406080b8ec3d5b9ef8eb2279bac32529e4f7b41cfefd5340e2", size = 1031549, upload-time = "2026-09-24T16:58:24.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/66/f5053862cbeddf37b3184870fa179a3e9d9e27e668c720bb7ab28b65b753/pymongo-4.18.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:930e8bcdf781a73d3df05976f5eaca1193102571cb91726e30009d64878ef8ce", size = 1054025, upload-time = "2026-09-24T16:58:26.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/29/4d7c9d7cb718ea7bbac56465de3aea5f79d1266683c16bdcd40e77746530/pymongo-4.18.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a695bd34d1e942eca93a186e679be196e288370f1a203e383b2187c06349e348", size = 1046337, upload-time = "2026-09-24T16:58:27.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/aa/c18c564883c5b2616744d1ee7ab073a3b4045aaa4bbbd524105448d4f630/pymongo-4.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d95e339e94b82950bfd62ec9ff8fea9bc8dfd9db0940e9134d1647d366560df5", size = 1032845, upload-time = "2026-09-24T16:58:29.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/16/f8a1cb7ac3f1c2c45649f18b7cb398779b4b45e1954bbfcb9317c74c744d/pymongo-4.18.2-cp311-cp311-win32.whl", hash = "sha256:2e2a3b816270c493f6645a168150ecbfb19a0bd0d9d3b56be728a847954da717", size = 814203, upload-time = "2026-09-24T16:58:30.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/53/e9d6350ea753b7a72d0346fb2c8e0c589e0eb443b5041989f3b4ea52ce55/pymongo-4.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:812b4ad5fe577f829cdb4e39a5601025b9cd7f2e3f8cd373cd5ab64ce78b0fb0", size = 821265, upload-time = "2026-09-24T16:58:32.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2a/be9693eca04bb6f53f3bdb027220a210bd429c7bf902b79538471849297e/pymongo-4.18.2-cp311-cp311-win_arm64.whl", hash = "sha256:c36cadbc3a7712575900c6f50cdf24ebe2b376b6ad643cb5df1b3dce93447eae", size = 815444, upload-time = "2026-09-24T16:58:34.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"name": "prowler",
|
||||
"displayName": "Prowler Cloud",
|
||||
"version": "0.1.0",
|
||||
"description": "Connect Claude to Prowler Cloud through the Prowler MCP server. Reads providers, scans, findings, compliance results and resources for AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and the other providers listed at https://registry.prowler.com, and returns remediation guidance for failed checks. Writes are limited to Prowler Cloud itself: triggering scans, finding triage, mute rules and the mutelist, providers and integrations. Your cloud resources are not changed by the connector.",
|
||||
"description": "Prowler Cloud security and compliance skills for Claude, powered by the Prowler MCP server. Triage compliance gaps and remediate findings on AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and many more providers listed at https://registry.prowler.com, plus custom providers, until your account meets the framework you choose.",
|
||||
"author": {
|
||||
"name": "Prowler",
|
||||
"email": "support@prowler.com",
|
||||
"url": "https://prowler.com"
|
||||
},
|
||||
"homepage": "https://docs.prowler.com/user-guide/ai-agents",
|
||||
"homepage": "https://docs.prowler.com",
|
||||
"repository": "https://github.com/prowler-cloud/prowler",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Prowler Cloud
|
||||
# Prowler for Claude Code
|
||||
|
||||
Cloud security and compliance in Claude, powered by the [Prowler MCP server](https://docs.prowler.com/projects/prowler-mcp/). Claude reads your Prowler Cloud providers, scans, findings and compliance results, returns Prowler's remediation guidance for each failed check, and manages triage and mute state. The connector never changes your cloud resources: any fix is applied with your own tools (Terraform, a cloud CLI or the console), and only after you approve it.
|
||||
End-to-end cloud security and compliance from inside [Claude Code](https://www.claude.com/product/claude-code), powered by the [Prowler MCP server](https://docs.prowler.com/projects/prowler-mcp/). The plugin lets Claude walk a Prowler Cloud-connected account through a compliance assessment and remediate findings until the chosen security or industry framework is compliant.
|
||||
|
||||
> **Preview**: this plugin is under active development. Report issues at <https://github.com/prowler-cloud/prowler/issues> or join the [Slack community](https://goto.prowler.com/slack).
|
||||
|
||||
@@ -70,7 +70,7 @@ Claude tracks progress in a markdown report under `.prowler/` at your project ro
|
||||
|
||||
## Data and privacy
|
||||
|
||||
The plugin stores nothing itself. It connects to the Prowler MCP server at `https://mcp.prowler.com/mcp`, authenticating with the API key you provide (kept in your system keychain and sent only as a bearer token to that server). Through it, Claude reads your Prowler Cloud providers, scans, findings, compliance results and resource metadata. It can also write to Prowler Cloud: trigger scans, set finding triage, create mute rules, replace the mutelist, and add or remove providers and integrations. Mutes and deletions are marked as destructive, so your client can ask before running them. It also queries the public Prowler Hub and Prowler documentation. The remediation report is written locally under `.prowler/` in your project. Nothing is sent to any other service. See the [Prowler privacy policy](https://prowler.com/privacy-policy) for how Prowler Cloud handles account data.
|
||||
The plugin stores nothing itself. It connects to the Prowler MCP server at `https://mcp.prowler.com/mcp`, authenticating with the API key you provide (kept in your system keychain and sent only as a bearer token to that server). Through it, Claude reads your Prowler Cloud providers, scans, findings, compliance results and resource metadata, and can trigger scans. It also queries the public Prowler Hub and Prowler documentation. The remediation report is written locally under `.prowler/` in your project. Nothing is sent to any other service. See the [Prowler privacy policy](https://prowler.com/privacy-policy) for how Prowler Cloud handles account data.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
|
||||
+1
-3
@@ -80,8 +80,6 @@ Prowler Features are considered proper nouns. They are to be referenced without
|
||||
|
||||
Prowler ships two product families. Use these names exactly; the former names Prowler App (now Prowler Local Server) and Prowler Enterprise (now Prowler Private Cloud) must not appear in new writing. The only allowed former-name notes are on the Prowler Product Families page (`getting-started/products/index.mdx`) and in the site-wide banner, which document the mapping.
|
||||
|
||||
Web UI guides are written for Prowler Cloud. Steps that open the application link to `https://cloud.prowler.com/sign-in`, and the scope is stated once at the top of the section with the AppliesTo banner (see [AppliesTo Banner for Product Scope](#appliesto-banner-for-product-scope)). Do not offer Prowler Local Server as an inline alternative in a step list ("Go to Prowler Cloud or launch Prowler Local Server"). Mention Prowler Local Server inline only where its behavior differs, for example a feature that is not available or an environment variable that must be set.
|
||||
|
||||
Prowler Products:
|
||||
|
||||
* **Prowler Cloud**
|
||||
@@ -575,7 +573,7 @@ The default covers Prowler Cloud, Prowler Private Cloud, and Prowler Local Serve
|
||||
<AppliesTo products={["Prowler Cloud", "Prowler Private Cloud"]} />
|
||||
```
|
||||
|
||||
Place it on its own line below the Version Badge when one is present, otherwise directly after the imports. On a page that also covers Prowler CLI, such as the provider getting-started guides, place it under the `## Prowler Cloud` heading instead, so it scopes only the web UI section.
|
||||
Place it on its own line below the Version Badge when one is present, otherwise directly after the imports.
|
||||
|
||||
### Cloud Marker for Subscription Content
|
||||
|
||||
|
||||
@@ -4,149 +4,6 @@ description: "New features and improvements in each Prowler release"
|
||||
rss: true
|
||||
---
|
||||
|
||||
<Update label="v5.47.0" description="October 29, 2026">
|
||||
### 🧭 Add Provider — One Step to Connect Any Provider
|
||||
|
||||
The add-provider wizard follows the same model for every provider: one connect step, then the launch step. Adding an Azure subscription used to take six screens; now the account, its alias and its credentials are one form, and a single submit registers the account, stores the secret and tests the connection.
|
||||
|
||||
- A failed connection test shows inline, keeps the values and turns the action into **Retry connection** without registering the account twice.
|
||||
- Access methods show as cards on the same step for AWS, GCP, Microsoft 365, GitHub, Alibaba Cloud and Cloudflare, and AWS, Azure and GCP switch to their organization flow through tabs.
|
||||
- Users with the Manage Account permission can invite a teammate to finish the connection for every provider, not only AWS.
|
||||
- **Ask Lighthouse AI** opens beside the wizard without closing it. It guides the connection from the Prowler docs by default and, when asked, connects the provider itself after confirming the provider, UID and credential field names, never echoing a secret back.
|
||||
- Product tours and the guided onboarding sequence are removed; new tenants still land on the add-provider wizard.
|
||||
|
||||
### 🎯 Issues — Verdicts Explained in the List
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
- The issue list leads with the verdict, shows the attack path under each title and adds a **Why** column with the five evidence legs, so the reason for a verdict reads without opening the issue. The summary tiles become one line with the folded findings and the findings already covered by a control.
|
||||
- A row opens in the side panel, next to Lighthouse AI, with the verdict, legs, remediation, triage and an **Attack path** tab with the issue graph, instead of expanding the issue inline.
|
||||
- The menu entry is renamed to **Exploitability issues**.
|
||||
|
||||
### ☁️ Azure and AWS Checks
|
||||
|
||||
- `network_application_gateway_waf_enabled` and `network_application_gateway_http2_enabled` cover Azure Application Gateways and their WAF policies, mapped to CIS 5.0 and 6.0. Contributed by @BabafemiB. Thank you!
|
||||
- `iam_subscription_owners_between_2_and_3` verifies that each Azure subscription has between two and three Owners, counting owners inherited from management groups, mapped to CIS 5.0 and 6.0. Contributed by @BabafemiB. Thank you!
|
||||
- The AWS security group internet exposure checks treat CIDRs in one ingress rule that together cover `0.0.0.0/0` or `::/0`, such as `0.0.0.0/1` plus `128.0.0.0/1`, as open to the internet; such a rule used to pass `ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22` and its siblings. Contributed by @harshrajdebug. Thank you!
|
||||
|
||||
Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
|
||||
|
||||
### 🛠️ Improvements and Fixes
|
||||
|
||||
- Every MCP Server tool declares `readOnlyHint` and `destructiveHint`, so clients can tell a listing from a write when deciding whether to ask the user. Mutelist and mute rule writes are marked destructive because they hide findings for good.
|
||||
- The cross-provider compliance tab keeps the frameworks that loaded, with a warning, when one catalog page or framework fails, instead of going blank.
|
||||
- The NATO D32 compliance framework has its own icon on the compliance cards.
|
||||
- Lighthouse AI replies no longer blink when the response finishes streaming, and the user avatar no longer requests the current page as an image, which rendered every page twice.
|
||||
- The Invitations page uses **Status** instead of **State**, and the Invitations and Roles filters show capitalized options.
|
||||
- **Prowler Cloud:** actions refused because the tenant is over its plan's usage limit, such as launching a scan, saving a schedule, adding a provider or triaging a finding, now link to the plans page.
|
||||
- **Prowler Cloud:** creating API keys is a Prowler Cloud feature; on Local Server the button opens the upgrade dialog, and existing keys can still be edited and revoked.
|
||||
- **Prowler Cloud:** issue evaluation after a scan reads the scan's findings through their indexes instead of a server-side cursor that took hours on large scans, so the evaluations no longer queue up behind each other.
|
||||
- **Prowler Private Cloud:** Lighthouse AI can use Amazon Bedrock with only a region, relying on the installation's own AWS role, when the API allows it.
|
||||
|
||||
### 🔐 Security Updates
|
||||
|
||||
- `pymongo` upgraded to 4.18.3, `dnspython` to 2.9.0 and `oauthlib` to 4.0.0 in the SDK and the API container image, patching the BSON decoding and DNS compression pointer denial of service advisories and the OAuth device flow token disclosure. The API also moves `django-allauth` to 65.19.7, since earlier releases cap `oauthlib` below 4.
|
||||
- `Authlib` upgraded to 1.9.1 and `dnspython` to 2.9.0 in the MCP Server container image, patching CVE-2026-96760 (critical), CVE-2026-114615, CVE-2026-114616 and CVE-2026-114605.
|
||||
|
||||
### 🙌 External Contributors
|
||||
|
||||
Thank you to our community contributors for this release!
|
||||
|
||||
- @BabafemiB: Azure `network_application_gateway_waf_enabled`, `network_application_gateway_http2_enabled` and `iam_subscription_owners_between_2_and_3` checks ([#13160](https://github.com/prowler-cloud/prowler/pull/13160), [#13145](https://github.com/prowler-cloud/prowler/pull/13145))
|
||||
- @harshrajdebug: AWS security group internet exposure fix for split CIDRs ([#13191](https://github.com/prowler-cloud/prowler/pull/13191))
|
||||
|
||||
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.47.0) for the complete list of changes.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.46.0" description="October 26, 2026">
|
||||
### 🎯 Issues — Attack Paths with an Exploitability Verdict
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
A scan returns a list of failed checks, and most of them are one problem seen from several angles. The new **Issues** page folds the failed findings of each account into attack paths and gives each one a verdict: `exploitable`, `likely`, `conditional`, `not_exploitable` or `unknown`. The verdict is drawn from five legs of evidence (reachability, access, target, controls and verification), each naming the checks and resources it was read from, so a reader can disagree with it instead of having to trust it. A control in force moves a verdict one rung down and the issue says which control did it; a check that did not run reads as `unknown`, never as safe.
|
||||
|
||||
- Patterns cover AWS, Azure, GCP and Microsoft 365 and are keyed on Prowler check IDs, so detection is never reimplemented. Six more AWS paths ship in this release: Lambda functions anyone can invoke, snapshots shared with every AWS account, instances that can leak their role's credentials, database ports open to the internet, stale or weakly protected IAM user credentials, and roles another AWS account can use.
|
||||
- Each issue says what the path buys an attacker and where the attacker has to start, lists the compliance frameworks its findings put at risk, and can be sent to Jira as one grouped ticket.
|
||||
- An attack path can be triaged with a status and notes from its detail. The decision is kept per account and pattern, so it survives the next scan, which marks it resolved when the path is gone and reopened when it comes back.
|
||||
- The issue list opens with summary tiles and names the account of each issue, so two accounts with the same misconfiguration no longer read as one row. Each issue shows its risk factors, the triage status of every folded finding and an issue graph that starts internet-facing paths at the Internet node, drawn only where a reachability check failed on the resource, and has a page of its own to share.
|
||||
- The Overview shows a verdict share card and the top issues with their risk factors, with counts scoped to the latest completed scan of each provider.
|
||||
- **API:** `GET /api/v1/issues`, `GET /api/v1/issues/{id}` and `GET /api/v1/issues/overview`, with `filter[outcome__in]`, `filter[attacker_start__in]` and `filter[triage_status__in]`; triage lives at `/api/v1/issues/{id}/triage` and `/api/v1/issues/{id}/triage/notes`.
|
||||
|
||||
### 🐘 PHP — Coverage After the October Security Release
|
||||
|
||||
The PHP project shipped an emergency release on October 21 fixing two unauthenticated remote code execution flaws (CVE-2026-109618, CVE-2026-109619), with no fix for 8.1 and older branches. Four new checks cover how each cloud exposes the runtime, and the Azure runtime checks no longer fail every version newer than the configured one.
|
||||
|
||||
- **AWS:** `elasticbeanstalk_environment_php_platform_updated` flags PHP environments on branches without security fixes or on platform versions older than the one bundling the fixed release, `lightsail_instance_php_blueprint_updated` reports instances created from PHP blueprints for manual verification, and `wafv2_webacl_managed_rule_groups_enabled` verifies that a Web ACL references at least one managed rule group.
|
||||
- **GCP:** new `appengine` service and `appengine_version_php_runtime_supported` check, flagging App Engine serving versions on PHP runtimes without security fixes.
|
||||
- **Azure:** `app_ensure_php_version_is_latest`, `app_ensure_python_version_is_latest` and `app_ensure_java_version_is_latest` now pass any runtime at or above the configured minimum, and the default `php_latest_version` is raised from `8.2` to `8.5` in the SDK and in the scan configuration template of Prowler App.
|
||||
|
||||
The version thresholds are configurable; see the [configuration file documentation](https://docs.prowler.com/user-guide/cli/tutorials/configuration_file).
|
||||
|
||||
### 🧠 GCP — Vertex AI Workbench Coverage
|
||||
|
||||
A new Vertex AI service audits Workbench instances with five checks, all contributed by @rajeshnadiminti. Thank you!
|
||||
|
||||
- `vertexai_workbench_instance_public_ip_disabled` and `vertexai_workbench_instance_root_access_disabled`, mapped to PCI DSS 4.0.
|
||||
- `vertexai_workbench_instance_terminal_access_disabled`, `vertexai_workbench_instance_file_download_disabled` and `vertexai_workbench_instance_auto_upgrade_enabled`, with the Workbench checks mapped to the CCC MLDE requirements.
|
||||
|
||||
### 🤖 AWS — Bedrock and Cognito Checks
|
||||
|
||||
- `bedrock_guardrail_sensitive_information_filter_enforced` flags guardrails whose PII entities or custom regexes take no action on the output path, so a detected match is reported in the trace and still delivered to the caller. Contributed by @tamg-aws. Thank you!
|
||||
- `bedrock_model_invocation_logging_text_data_enabled` verifies that model invocation logging delivers prompt and response text rather than naming a destination while excluding request and response bodies. Contributed by @tamg-aws. Thank you!
|
||||
- `cognito_user_pool_account_recovery_sms_not_preferred` verifies that user pools do not rank SMS first for account recovery. Contributed by @UTKARSH698. Thank you!
|
||||
- `bedrock_api_key_no_long_term_credentials` now reports long-term API keys that never expire; IAM only returns `ExpirationDate` for keys created with an expiration period, so keys created without one produced no finding at all. Also contributed by @tamg-aws.
|
||||
|
||||
Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
|
||||
|
||||
### 📬 Google Workspace — Policy Precedence and Edition Context
|
||||
|
||||
- Checks now read the policy Google applies (the greatest `sortOrder`) instead of the last policy the Cloud Identity Policy API lists, which reported default values over admin-configured ones.
|
||||
- Checks for Chat, Drive, Gmail, Calendar and Groups pass with the reason stated when the app is turned off for everyone and no organizational unit or group turns it back on. Drive sharing settings and the settings of existing groups keep being evaluated, because existing shares and groups stay reachable.
|
||||
- Findings state when a feature is not sold on the tenant edition, such as Drive DLP rules, Google session control and the shared drive sharing settings on Business Starter. Reading editions needs the optional `apps.licensing` scope; see the [authentication documentation](https://docs.prowler.com/user-guide/providers/googleworkspace/authentication).
|
||||
- The Gmail spoofing checks read each protection action from its own Policy API field: the inbound domain spoofing check could never pass and the similar domain names check reported the inbound action.
|
||||
- `drive_publishing_files_disabled` passes when sharing outside the organization is limited to allowlisted domains or off, and `drive_desktop_access_disabled` remediation points to the Google Drive for desktop card in the Admin console.
|
||||
|
||||
### 🛠️ Improvements and Fixes
|
||||
|
||||
- Scan report generation parses each resource's metadata once per batch, sanitizes it once per finding and releases each batch before building the next one, so providers with very large resource metadata no longer exhaust the worker memory.
|
||||
- Scan ingestion compares the serialized resource metadata with the stored value and serializes sets in a stable order, so unchanged resources no longer get their metadata, details and `updated_at` rewritten on every scan.
|
||||
- The compliance report PDF cover table wraps every value, so long account IDs such as OCI tenancy OCIDs no longer overflow their cell.
|
||||
- **Launch an initial scan now** is ticked by default on the provider and organization setup launch steps, so saving a schedule also runs a first scan unless the box is unticked.
|
||||
- The SAML SSO integration card on the profile page is shown to roles with the Manage Account permission, matching the API requirement.
|
||||
- **Prowler Cloud:** the scan-reports worker replaces each Celery child process after one report, so the memory a large report needs is returned instead of pushing the task towards an out-of-memory kill, and a container health check replaces a worker that stops consuming instead of leaving it in steady state.
|
||||
- **Prowler Cloud:** persistent report, compliance and overview workers no longer shut themselves down when a task named like a scan hits its time limit or is revoked; the one-shot shutdown only applies to the ephemeral workers of the `scans` queue.
|
||||
|
||||
### 🔐 Security Updates
|
||||
|
||||
- Resource metadata attached to findings no longer carries the values that secrets checks scan (CloudFormation outputs, Lambda environment variables, EC2 user data and similar fields); they are replaced with `REDACTED` before reaching the Evidence tab, the API and the output files.
|
||||
- SAML SSO configuration endpoints require the `manage_account` permission instead of `manage_integrations`, since the configured IdP controls who signs in and which role they receive.
|
||||
- Attack Paths custom queries strip block comments before the blocked-operation check and drop APOC virtual nodes and relationships from results, so `LOAD CSV` can no longer be split with a comment nor its response returned under a forged provider label.
|
||||
- `GET /resources/metadata`, `GET /resources/metadata/latest`, `GET /findings/metadata` and `GET /findings/metadata/latest` honour the provider groups of the caller role instead of aggregating every provider in the tenant.
|
||||
- OpenStack `auth_url` values resolving to loopback, private or otherwise non-public hosts are rejected before the SDK connects, on both the connection test and the scan.
|
||||
- `djangorestframework` upgraded to 3.18.3 in the API, patching the JSON parser charset and Accept header denial of service and the TokenProxy revocation bypass.
|
||||
- `Pillow` upgraded to 13.0.0 in the API, patching a JPEG decoder heap overflow.
|
||||
- `certifi` upgraded to 2026.10.19 in the SDK, the API and the MCP Server, dropping the emSign root CAs whose intermediate keys were compromised, and `uv` to 0.13.4 in the SDK and API images for the same reason.
|
||||
- `aiohttp` upgraded to 3.14.5 with `yarl` 1.25.1 in the SDK and the API, patching the aiohttp resource exhaustion advisories.
|
||||
- `black` upgraded to 26.10.1 and `graphql-core` to 3.2.12 in the SDK, patching CVE-2026-114134, CVE-2026-75507 and CVE-2026-75508, and `libbrotli1` to 1.1.0-2+deb13u1 in its container image, patching CVE-2026-111497.
|
||||
- Trivy upgraded to 0.76.1 in the SDK and API images, patching the HTTP/2, TLS and Go 1.27.2 stdlib CVEs bundled in Trivy 0.75.0, and the SDK and API images no longer ship the MicrosoftTeams build SBOM that Trivy read as installed `zod` and `body-parser`.
|
||||
- `next` upgraded to 16.4.1 in the UI, patching two unauthenticated remote code executions (Image Optimization with SVG files, GHSA-2464-fhr4-w9xw; `use cache` functions returning typed arrays, GHSA-673q-mjm6-r5xr), `@sentry/nextjs` to 10.75.2 for the middleware bypass through the tunnel route (GHSA-mchm-gjx6-7h55) and DOMPurify to 3.4.17 for the `SAFE_FOR_TEMPLATES` bypass (GHSA-cg2g-xh64-v4q7).
|
||||
- Node.js upgraded to 24.22.1 LTS in the UI container image, patching the eight CVEs of the October 2026 security release that affect the 24.x line, including the Brotli decoder remote code execution (CVE-2026-111497); the base image moves to Alpine 3.24.
|
||||
|
||||
### 🙌 External Contributors
|
||||
|
||||
Thank you to our community contributors for this release!
|
||||
|
||||
- @rajeshnadiminti: GCP Vertex AI service and its Workbench checks ([#13062](https://github.com/prowler-cloud/prowler/pull/13062), [#13077](https://github.com/prowler-cloud/prowler/pull/13077))
|
||||
- @tamg-aws: AWS `bedrock_guardrail_sensitive_information_filter_enforced` and `bedrock_model_invocation_logging_text_data_enabled` checks and the `bedrock_api_key_no_long_term_credentials` fix ([#12663](https://github.com/prowler-cloud/prowler/pull/12663))
|
||||
- @UTKARSH698: AWS `cognito_user_pool_account_recovery_sms_not_preferred` check ([#12251](https://github.com/prowler-cloud/prowler/pull/12251))
|
||||
|
||||
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.46.0) for the complete list of changes.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.45.0" description="October 15, 2026">
|
||||
### 🤖 Azure — AI Services Coverage
|
||||
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
title: "Installation"
|
||||
---
|
||||
|
||||
<Tip>
|
||||
[Prowler Cloud](https://cloud.prowler.com/sign-up) runs the same application with no installation or maintenance. Follow the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) to start scanning right away.
|
||||
</Tip>
|
||||
|
||||
### Installation
|
||||
|
||||
Prowler Local Server offers flexible installation methods tailored to various environments.
|
||||
|
||||
Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detailed usage instructions, which apply to Prowler Local Server as well.
|
||||
Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detailed usage instructions.
|
||||
|
||||
<Warning>
|
||||
Prowler configuration is based on `.env` files. Every version of Prowler can have differences on that file, so, please, use the file that corresponds with that version or repository branch or tag.
|
||||
@@ -132,8 +128,8 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.47.0"
|
||||
PROWLER_API_VERSION="5.47.0"
|
||||
PROWLER_UI_VERSION="5.45.0"
|
||||
PROWLER_API_VERSION="5.45.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: 'Overview'
|
||||
---
|
||||
|
||||
Prowler Local Server is a self-hosted web application that simplifies running Prowler. It is the open source counterpart of [Prowler Cloud](/getting-started/products/prowler-cloud), which runs the same application as a managed service. It provides:
|
||||
Prowler Local Server is a self-hosted web application that simplifies running Prowler. It provides:
|
||||
|
||||
- **User-friendly interface** for configuring and executing scans
|
||||
- Dashboard to **view results** and manage **security findings**
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
@@ -4,10 +4,6 @@
|
||||
|
||||

|
||||
|
||||
<Card title="Get started with Prowler Cloud" icon="cloud" href="https://cloud.prowler.com/sign-up">
|
||||
The fastest way to run Prowler: create an account, connect a provider, and get findings without installing anything. No billing details are required during the trial period.
|
||||
</Card>
|
||||
|
||||
Prowler ships two product families: Prowler Products, operated or licensed by the Prowler team, and Open Source projects, free to run and extend. See [Prowler product families](/getting-started/products) for every official name, including former names.
|
||||
|
||||
### Prowler Products
|
||||
@@ -29,8 +25,6 @@ Prowler ships two product families: Prowler Products, operated or licensed by th
|
||||
|
||||
### Open Source
|
||||
|
||||
Free to run and extend. Prefer the Open Source projects to run Prowler from a terminal, in CI/CD, or on infrastructure you operate yourself.
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Prowler CLI" icon="terminal" href="/getting-started/products/prowler-cli">
|
||||
Command line tool to run security scans across all supported providers.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: 'Compliance'
|
||||
sidebarTitle: 'Overview'
|
||||
description: 'Run security checks against compliance frameworks, review posture across providers, use Compliance Watchlist to pin frameworks, and download CSV or PDF reports from Prowler Cloud, or CSV reports from Prowler CLI.'
|
||||
description: 'Run security checks against compliance frameworks, review posture across providers, use Compliance Watchlist to pin frameworks, and download CSV or PDF reports from Prowler Cloud and Prowler Local Server, or CSV reports from Prowler CLI.'
|
||||
---
|
||||
|
||||
import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
|
||||
@@ -24,15 +24,15 @@ For the unified compliance score methodology used across frameworks, see [Prowle
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Prowler Cloud
|
||||
## Prowler Cloud and Prowler Local Server
|
||||
|
||||
The Compliance section in Prowler Cloud centralizes compliance posture across every connected provider. It aggregates scan results, surfaces Prowler ThreatScore, and exposes detailed requirement-level evidence for each supported framework.
|
||||
The Compliance section in Prowler Cloud and Prowler Local Server centralizes compliance posture across every connected provider. It aggregates scan results, surfaces Prowler ThreatScore, and exposes detailed requirement-level evidence for each supported framework.
|
||||
|
||||
### Accessing the Compliance Section
|
||||
|
||||
To open the compliance overview, follow these steps:
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in).
|
||||
1. Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) or to a Prowler Local Server instance.
|
||||
2. Select **Compliance** from the left navigation.
|
||||
|
||||
The Compliance page is organized into two tabs:
|
||||
@@ -42,10 +42,10 @@ The Compliance page is organized into two tabs:
|
||||
|
||||
<SubscriptionBanner label="Multiple Scans tab" />
|
||||
|
||||
<img src="/images/compliance/prowler-app-compliance-overview.png" alt="Compliance overview page in Prowler Cloud showing filters, the Prowler ThreatScore card, and the framework grid" width="900" />
|
||||
<img src="/images/compliance/prowler-app-compliance-overview.png" alt="Compliance overview page in Prowler Cloud and Prowler Local Server showing filters, the Prowler ThreatScore card, and the framework grid" width="900" />
|
||||
|
||||
<Note>
|
||||
Compliance results require at least one completed scan. If no scan has finished yet, Prowler Cloud displays a notice prompting to launch or wait for a scan to complete.
|
||||
Compliance results require at least one completed scan. If no scan has finished yet, Prowler Cloud and Prowler Local Server display a notice prompting to launch or wait for a scan to complete.
|
||||
</Note>
|
||||
|
||||
### Filtering Compliance Results
|
||||
@@ -196,7 +196,7 @@ Frameworks without a custom layout fall back to the generic details panel, which
|
||||
|
||||
### Downloading Compliance Reports
|
||||
|
||||
Prowler Cloud exposes two formats:
|
||||
Prowler Cloud and Prowler Local Server expose two formats:
|
||||
|
||||
* **CSV report:** Every requirement, every check, and every finding for the selected scan and filters. Available for all supported frameworks.
|
||||
* **PDF report:** Curated executive-style report. Currently supported for Prowler ThreatScore, ENS RD2022, NIS2, and CSA CCM. Additional PDF reports are added in subsequent Prowler releases.
|
||||
@@ -234,7 +234,7 @@ Use the API to integrate compliance evidence into ticketing systems, executive d
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
Prowler CLI evaluates the same compliance frameworks as Prowler Cloud, and produces detailed CSV outputs alongside the standard scan results. By default, it runs every supported framework and prints a status summary at the end of the scan:
|
||||
Prowler CLI evaluates the same compliance frameworks as Prowler Cloud and Prowler Local Server, and produces detailed CSV outputs alongside the standard scan results. By default, it runs every supported framework and prints a status summary at the end of the scan:
|
||||
|
||||
<img src="/images/cli/compliance/compliance.png" />
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ title: 'Getting Started With Alibaba Cloud on Prowler'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler supports Alibaba Cloud both from the CLI and from Prowler Cloud. This guide walks you through the requirements, how to connect the provider in the UI, and how to run scans from the command line.
|
||||
|
||||
@@ -30,8 +29,6 @@ Before you begin, make sure you have:
|
||||
|
||||
<VersionBadge version="5.18.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Get Your Alibaba Cloud Account ID
|
||||
|
||||
1. Log in to the [Alibaba Cloud Console](https://home.console.alibabacloud.com/)
|
||||
@@ -42,7 +39,7 @@ Before you begin, make sure you have:
|
||||
|
||||
### Step 2: Access Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Go to "Configuration" > "Providers"
|
||||
|
||||

|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
title: 'Getting Started With AWS on Prowler'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/RPgIWOCERzY" title="Prowler Cloud Onboarding AWS" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
|
||||
|
||||
> Walkthrough video onboarding an AWS Account using Assumed Role.
|
||||
@@ -22,7 +18,7 @@ import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
### Step 2: Access Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Go to "Configuration" > "Providers"
|
||||
|
||||

|
||||
@@ -79,7 +75,7 @@ For detailed instructions on how to create the role, see [Authentication > Assum
|
||||
|
||||

|
||||
|
||||
9. Paste the ARN into the corresponding field in Prowler Cloud
|
||||
9. Paste the ARN into the corresponding field in Prowler Cloud or Prowler Local Server
|
||||
|
||||

|
||||
|
||||
@@ -114,7 +110,7 @@ AWS accounts can also be configured using static credentials (not recommended fo
|
||||
|
||||
For detailed instructions on how to create the credentials, see [Authentication > Credentials](/user-guide/providers/aws/authentication#credentials).
|
||||
|
||||
1. Complete the form in Prowler Cloud and click "Next"
|
||||
1. Complete the form in Prowler Cloud or Prowler Local Server and click "Next"
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
title: 'Getting Started With Azure on Prowler'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/v1as8vTFlMg" title="Prowler Cloud Onboarding Azure" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
|
||||
> Walkthrough video onboarding an Azure Subscription using Service Principal.
|
||||
|
||||
@@ -38,7 +34,7 @@ For detailed instructions on how to create the Service Principal and configure p
|
||||
|
||||
### Step 2: Access Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Navigate to `Configuration` > `Providers`
|
||||
|
||||

|
||||
|
||||
@@ -3,7 +3,6 @@ title: 'Getting Started With Cloudflare on Prowler'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler for Cloudflare scans zones for security misconfigurations, including SSL/TLS settings, DNSSEC, HSTS, WAF rules, DNS records, and more.
|
||||
|
||||
@@ -37,8 +36,6 @@ Both links open the Cloudflare Dashboard with the seven required read-only scope
|
||||
|
||||
<VersionBadge version="5.19.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Locate the Account ID
|
||||
|
||||
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
|
||||
@@ -53,7 +50,7 @@ The Account ID is a 32-character hexadecimal string (e.g., `372e67954025e0ba6aaa
|
||||
|
||||
### Step 2: Open Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up).
|
||||
1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
2. Navigate to "Configuration" > "Providers".
|
||||
|
||||

|
||||
|
||||
@@ -89,7 +89,7 @@ This method uses the Google Cloud CLI to authenticate and is suitable for develo
|
||||
|
||||

|
||||
|
||||
8. Extract the following values for Prowler Cloud:
|
||||
8. Extract the following values for Prowler Cloud or Prowler Local Server:
|
||||
|
||||
- `client_id`
|
||||
- `client_secret`
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
title: 'Getting Started With GCP on Prowler'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Get the GCP Project ID
|
||||
|
||||
1. Go to the [GCP Console](https://console.cloud.google.com/)
|
||||
@@ -17,7 +13,7 @@ import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
### Step 2: Access Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Go to "Configuration" > "Providers"
|
||||
|
||||

|
||||
|
||||
@@ -272,7 +272,7 @@ Store the `.pem` private key securely. Anyone with this key can authenticate as
|
||||
|
||||
## Prowler Cloud Authentication
|
||||
|
||||
For step-by-step setup instructions for Prowler Cloud, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloud).
|
||||
For step-by-step setup instructions for Prowler Cloud, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloud-and-prowler-local-server).
|
||||
|
||||
### Using Personal Access Token
|
||||
|
||||
@@ -302,7 +302,7 @@ For step-by-step setup instructions for Prowler Cloud, see the [Getting Started
|
||||
|
||||
3. Enter your GitHub App ID and upload the private key (`.pem` file).
|
||||
|
||||
For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloud).
|
||||
For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloud-and-prowler-local-server).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
title: 'Getting Started with GitHub'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
This guide covers setting up GitHub security scanning with Prowler. Choose a preferred interface below:
|
||||
|
||||
<Note>
|
||||
@@ -18,7 +16,7 @@ Prowler can scan either:
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Prowler Cloud" icon="cloud" href="#prowler-cloud">
|
||||
<Card title="Prowler Cloud & Local Server" icon="cloud" href="#prowler-cloud-and-prowler-local-server">
|
||||
Web-based interface with centralized management
|
||||
</Card>
|
||||
<Card title="Prowler CLI" icon="terminal" href="#prowler-cli">
|
||||
@@ -28,9 +26,7 @@ Prowler can scan either:
|
||||
|
||||
---
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
## Prowler Cloud and Prowler Local Server
|
||||
|
||||
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/9ETI84Xpu2g" title="Prowler Cloud Onboarding Github" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
|
||||
|
||||
@@ -38,7 +34,7 @@ Prowler can scan either:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before adding GitHub to Prowler Cloud, ensure you have:
|
||||
Before adding GitHub to Prowler Cloud or Prowler Local Server, ensure you have:
|
||||
|
||||
1. **GitHub Account Access**
|
||||
- Personal GitHub account, OR
|
||||
@@ -50,9 +46,9 @@ Before adding GitHub to Prowler Cloud, ensure you have:
|
||||
- OAuth App Token
|
||||
- GitHub App Credentials (Not Recommended - limited data access)
|
||||
|
||||
### Step 1: Access Prowler Cloud
|
||||
### Step 1: Access Prowler Cloud or Prowler Local Server
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Go to **Configuration** → **Providers**
|
||||
|
||||

|
||||
|
||||
@@ -3,7 +3,6 @@ title: 'Getting Started With Google Workspace on Prowler'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler for Google Workspace audits the organization's Google Workspace environment for security misconfigurations, including super administrator account hygiene, domain settings, and more.
|
||||
|
||||
@@ -29,8 +28,6 @@ Set up authentication for Google Workspace with the [Google Workspace Authentica
|
||||
|
||||
<VersionBadge version="5.21.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Locate the Customer ID
|
||||
|
||||
1. Log into the [Google Workspace Admin Console](https://admin.google.com).
|
||||
@@ -45,7 +42,7 @@ The Customer ID starts with the letter "C" followed by alphanumeric characters (
|
||||
|
||||
### Step 2: Open Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up).
|
||||
1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
2. Navigate to "Configuration" > "Providers".
|
||||
|
||||

|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
title: "Getting Started with the IaC Provider"
|
||||
---
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler's Infrastructure as Code (IaC) provider enables scanning of local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks, allowing assessment of code before deployment.
|
||||
|
||||
@@ -36,15 +35,13 @@ Prowler IaC provider scans the following Infrastructure as Code configurations f
|
||||
|
||||
<VersionBadge version="5.14.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Supported Scanners
|
||||
|
||||
Scanner selection is not configurable in Prowler Cloud. Default scanners, misconfig and secret, run automatically during each scan.
|
||||
|
||||
### Step 1: Access Prowler Cloud
|
||||
### Step 1: Access Prowler Cloud or Prowler Local Server
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Go to "Configuration" > "Providers"
|
||||
|
||||

|
||||
|
||||
@@ -3,7 +3,6 @@ title: "Getting Started with the Image Provider"
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler's Image provider enables comprehensive container image security scanning by integrating with [Trivy](https://trivy.dev/). This provider detects vulnerabilities, exposed secrets, and misconfigurations in container images, converting Trivy findings into Prowler's standard reporting format for unified security assessment.
|
||||
|
||||
@@ -28,15 +27,13 @@ Prowler's Image provider enables comprehensive container image security scanning
|
||||
|
||||
<VersionBadge version="5.21.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Supported Scanners
|
||||
|
||||
Prowler Cloud does not support scanner selection. The vulnerability, secret, and misconfiguration scanners run automatically during each scan.
|
||||
|
||||
### Step 1: Access Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Navigate to "Configuration" > "Providers"
|
||||
|
||||

|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
title: 'Getting Started with Kubernetes'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
### Step 1: Access Prowler Cloud or Prowler Local Server
|
||||
|
||||
### Step 1: Access Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up)
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
|
||||
2. Go to "Configuration" > "Providers"
|
||||
|
||||

|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
title: 'Getting Started With Microsoft 365 on Prowler'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
<Note>
|
||||
**Government Cloud Support**
|
||||
|
||||
@@ -31,8 +29,6 @@ Set up authentication for Microsoft 365 with the [Microsoft 365 Authentication](
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Locate the Domain ID
|
||||
|
||||
1. Open the Entra ID portal, then search for "Domain" or go to Identity > Settings > Domain Names.
|
||||
@@ -45,7 +41,7 @@ Set up authentication for Microsoft 365 with the [Microsoft 365 Authentication](
|
||||
|
||||
### Step 2: Open Prowler Cloud
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up).
|
||||
1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
2. Navigate to "Configuration" > "Providers".
|
||||
|
||||

|
||||
|
||||
@@ -15,7 +15,7 @@ Before you begin, make sure you have:
|
||||
3. An **API Key pair** (public and private keys) with appropriate permissions:
|
||||
- **Organization Read Only**: Provides read-only access to everything in the organization, including all projects in the organization. This permission is sufficient for most security checks.
|
||||
- **Organization Owner**: Required to audit the [Auditing configuration](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-auditing) for projects. Database auditing tracks database operations and security events, including authentication attempts, data definition language (DDL) changes, user and role modifications, and privilege grants. This configuration is essential for security monitoring, forensics, and compliance. Without **Organization Owner** permission, the `projects_auditing_enabled` check cannot retrieve the audit configuration status.
|
||||
4. A [Prowler Cloud](https://cloud.prowler.com/sign-up) account, or Prowler CLI (`pip install prowler`).
|
||||
4. Access to Prowler Cloud or Prowler Local Server, or Prowler CLI (`pip install prowler`).
|
||||
|
||||
For detailed instructions on creating API keys, see the [MongoDB Atlas authentication guide](./authentication.mdx).
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
title: 'Getting Started with Oracle Cloud Infrastructure (OCI)'
|
||||
---
|
||||
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler supports security scanning of Oracle Cloud Infrastructure (OCI) environments. This guide will help you get started with using Prowler to audit your OCI tenancy.
|
||||
|
||||
## Prowler Cloud
|
||||
|
||||
<AppliesTo />
|
||||
The following steps apply to Prowler Cloud and Prowler Local Server.
|
||||
|
||||
### Step 1: Collect OCI Identifiers
|
||||
1. Sign in to the [OCI Console](https://cloud.oracle.com/) and open **Tenancy Details** to copy the Tenancy OCID.
|
||||
@@ -17,7 +15,7 @@ Prowler supports security scanning of Oracle Cloud Infrastructure (OCI) environm
|
||||
4. In **Tenancy Details**, note the **Home Region** identifier (for example, `me-abudhabi-1`). Any other region the tenancy is subscribed to also works.
|
||||
|
||||
### Step 2: Access Prowler Cloud
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up).
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
2. Go to **Configuration** → **Providers** and click **Add Provider**.
|
||||

|
||||
3. Select **Oracle Cloud** and enter the **Tenancy OCID** and an optional alias, then choose **Next**.
|
||||
@@ -344,7 +342,7 @@ prowler oci \
|
||||
|
||||
**Error: "OCI credential validation failed" with `401 NotAuthenticated` on `get_tenancy`**
|
||||
- The tenancy is not subscribed to the region used for validation (by default `us-ashburn-1`)
|
||||
- In Prowler Cloud, update the provider credentials and select the tenancy home region
|
||||
- In Prowler Cloud or Prowler Local Server, update the provider credentials and select the tenancy home region
|
||||
- In Prowler CLI, set `region` in `~/.oci/config` to the home region. All subscribed regions are still scanned; `--region` also works but limits the scan to that region
|
||||
|
||||
**Error: "Invalid region"**
|
||||
|
||||
@@ -3,7 +3,6 @@ title: 'Getting Started With Okta on Prowler'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler for Okta scans an Okta organization for identity and session-management misconfigurations. The provider authenticates as a service application using **OAuth 2.0 with a private-key JWT** (Client Credentials grant) — no end-user login, read-only by scope.
|
||||
|
||||
@@ -29,11 +28,9 @@ Set up authentication for Okta with the [Okta Authentication](/user-guide/provid
|
||||
|
||||
<VersionBadge version="5.28.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Add the Provider
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up).
|
||||
1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
2. Navigate to "Configuration" > "Providers".
|
||||
|
||||

|
||||
|
||||
@@ -3,7 +3,6 @@ title: "Getting Started With Vercel on Prowler"
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
Prowler for Vercel scans teams and projects for security misconfigurations, including deployment protection, environment variable exposure, WAF rules, domain configuration, team access controls, and more.
|
||||
|
||||
@@ -27,11 +26,9 @@ Set up authentication for Vercel with the [Vercel Authentication](/user-guide/pr
|
||||
|
||||
<VersionBadge version="5.23.0" />
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
### Step 1: Add the Provider
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in) or [create an account](https://cloud.prowler.com/sign-up).
|
||||
1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
2. Navigate to "Configuration" > "Providers".
|
||||
|
||||

|
||||
|
||||
@@ -99,7 +99,7 @@ export PROWLER_API_KEY="pk_example-api-key"
|
||||
|
||||
To create an API key:
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in)
|
||||
1. Log in to Prowler Cloud or Prowler Local Server
|
||||
2. Click **Profile** → **Account**
|
||||
3. Click **Create API Key**
|
||||
4. Provide a descriptive name and optionally set an expiration date
|
||||
|
||||
@@ -51,7 +51,7 @@ export PROWLER_API_KEY="pk_example-api-key"
|
||||
|
||||
To create an API key:
|
||||
|
||||
1. Sign in to [Prowler Cloud](https://cloud.prowler.com/sign-in)
|
||||
1. Log in to Prowler Cloud or Prowler Local Server
|
||||
2. Click **Profile** → **Account**
|
||||
3. Click **Create API Key**
|
||||
4. Provide a descriptive name and optionally set an expiration date
|
||||
|
||||
@@ -4,15 +4,22 @@ sidebarTitle: 'Getting Started'
|
||||
---
|
||||
|
||||
import { ProviderCards } from "/snippets/provider-cards.mdx"
|
||||
import { AppliesTo } from "/snippets/applies-to.mdx"
|
||||
|
||||
<AppliesTo />
|
||||
|
||||
**Prowler Cloud** is a web application that simplifies running Prowler. This tutorial will guide you through setting up and using it.
|
||||
|
||||
**Prowler Local Server** is the self-hosted version of **Prowler Cloud**. See [Prowler product families](/getting-started/products) for every official product name.
|
||||
|
||||
## Accessing Prowler Cloud and API Documentation
|
||||
|
||||
Sign in at [cloud.prowler.com](https://cloud.prowler.com/sign-in). The API documentation is available at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs).
|
||||
If you are a [Prowler Cloud](https://cloud.prowler.com/sign-in) user, you can access API docs at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs)
|
||||
|
||||
<Note>
|
||||
**For Prowler Local Server users**
|
||||
|
||||
After [installing](/getting-started/installation/prowler-app) **Prowler Local Server**, access it at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
To view the auto-generated **Prowler API** documentation, navigate to [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs). This documentation provides details on available endpoints, parameters, and responses.
|
||||
</Note>
|
||||
|
||||
## Step 1: Sign Up
|
||||
### Sign Up with Email
|
||||
@@ -78,14 +85,20 @@ For detailed instructions on configuring credentials for each provider, refer to
|
||||
<ProviderCards />
|
||||
|
||||
## Step 5: Test Connection
|
||||
To verify the credentials, click the connect action at the bottom of the same step (for example, "Connect account", "Connect subscription" or "Connect project"). Prowler saves the provider and its credentials and tests the connection in that single action. When the test succeeds, the wizard moves straight to launching the scan. When it fails, the reason appears above the form and the values stay in place: click "Retry connection" to test again, or correct a field, which brings back the connect action, and click it. Neither adds the provider twice.
|
||||
After adding your cloud account credentials, click the `Check connection` button to verify that Prowler can successfully connect to your provider:
|
||||
|
||||
<img src="/images/test-connection-button.png" alt="Test Connection" width="700" />
|
||||
|
||||
<Note>
|
||||
To delegate the connection, select "I don't have access, invite a teammate" on the same step when you cannot create the credentials yourself. The option is available for every provider to users who can manage the account. Prowler Local Server adds the invitation to the tenant and shows the link to share. Prowler Cloud also emails it.
|
||||
For a single AWS account, Prowler tests the connection as part of the `Connect account` step, so the wizard moves straight to launching the scan.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
To delegate the AWS connection, select `I don't have access, invite a teammate` on the same step when you cannot create the IAM role or do not have the account credentials. Prowler App sends the invitation to the tenant and shows the link to share. Prowler Cloud also emails it. This option is available to users who can manage the account.
|
||||
</Note>
|
||||
|
||||
## Step 6: Scan Started
|
||||
Once the connection test succeeds, the wizard moves to the launch step, since the provider is already saved. To start the first scan, click "Launch scan", or "Save and launch scan" when scanning on a schedule. The `Scans` section shows the scan in progress:
|
||||
After the connection check succeeds, save the provider and start your first scan with the `Launch Scan` button. The `Scans` section shows the scan in progress:
|
||||
|
||||
<img src="/images/provider-added.png" alt="Start Now" width="700" />
|
||||
|
||||
@@ -164,9 +177,3 @@ Once your scan has finished, you don’t need to grab the entire ZIP—just pull
|
||||
To fetch a single compliance report via API, see the Retrieve compliance report as CSV endpoint in the Prowler API Reference. [Prowler API Reference - Retrieve compliance report as CSV](https://api.prowler.com/api/v1/docs#tag/Scan/operation/scans_compliance_retrieve)
|
||||
|
||||
</Note>
|
||||
|
||||
## Running the Same Steps on Prowler Local Server
|
||||
|
||||
**Prowler Local Server** is the self-hosted version of **Prowler Cloud**, and every step in this guide works the same way on it. See [Prowler product families](/getting-started/products) for every official product name.
|
||||
|
||||
After [installing](/getting-started/installation/prowler-app) **Prowler Local Server**, access it at [http://localhost:3000](http://localhost:3000) and the auto-generated **Prowler API** documentation at [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs).
|
||||
@@ -4,18 +4,6 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [0.13.0] (Prowler v5.47.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `readOnlyHint` and `destructiveHint` annotations on every MCP tool, with mute rule and mutelist writes marked destructive [(#13196)](https://github.com/prowler-cloud/prowler/pull/13196)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Authlib from 1.7.2 to 1.9.1 and dnspython from 2.8.0 to 2.9.0 in the MCP Server container image, patching CVE-2026-96760 (critical), CVE-2026-114615, CVE-2026-114616 and CVE-2026-114605 [(#13186)](https://github.com/prowler-cloud/prowler/pull/13186)
|
||||
|
||||
---
|
||||
|
||||
## [0.12.4] (Prowler v5.46.0)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
@@ -30,8 +30,6 @@ class AttackPathsTools(BaseTool):
|
||||
- prowler_run_attack_paths_query: Execute query and analyze attack paths
|
||||
"""
|
||||
|
||||
read_only_tools = frozenset({"run_attack_paths_query"})
|
||||
|
||||
async def list_attack_paths_scans(
|
||||
self,
|
||||
provider_id: list[str] = Field(
|
||||
|
||||
@@ -2,16 +2,12 @@ import inspect
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
from prowler_mcp_server.prowler_app.utils.api_client import ProwlerAPIClient
|
||||
|
||||
READ_ONLY_PREFIXES = ("get_", "list_", "search_")
|
||||
|
||||
|
||||
class BaseTool(ABC):
|
||||
"""Abstract base class for all MCP tools.
|
||||
@@ -28,10 +24,6 @@ class BaseTool(ABC):
|
||||
Attributes:
|
||||
_api_client: Singleton instance of ProwlerAPIClient for API requests
|
||||
_logger: Logger instance for structured logging
|
||||
read_only_tools: Tools that change nothing although their name does not
|
||||
start with one of READ_ONLY_PREFIXES
|
||||
destructive_tools: Tools that remove, replace or hide data although their
|
||||
name does not start with "delete_"
|
||||
|
||||
Example:
|
||||
class FindingsTools(BaseTool):
|
||||
@@ -45,9 +37,6 @@ class BaseTool(ABC):
|
||||
return response
|
||||
"""
|
||||
|
||||
read_only_tools: frozenset[str] = frozenset()
|
||||
destructive_tools: frozenset[str] = frozenset()
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the tool.
|
||||
|
||||
@@ -104,24 +93,10 @@ class BaseTool(ABC):
|
||||
|
||||
# Check if the method is a coroutine function (async)
|
||||
if inspect.iscoroutinefunction(method):
|
||||
mcp.tool(method, annotations=self._annotations_for(name))
|
||||
mcp.tool(method)
|
||||
registered_count += 1
|
||||
self.logger.debug(f"Auto-registered tool: {name}")
|
||||
|
||||
self.logger.info(
|
||||
f"Auto-registered {registered_count} tools from {self.__class__.__name__}"
|
||||
)
|
||||
|
||||
def _annotations_for(self, name: str) -> ToolAnnotations:
|
||||
"""Return the MCP hints a client reads to decide whether to ask the user.
|
||||
|
||||
Every tool states both hints: left out, the MCP spec treats a tool as
|
||||
destructive, so a client could not tell a listing from a deletion.
|
||||
"""
|
||||
if name.startswith(READ_ONLY_PREFIXES) or name in self.read_only_tools:
|
||||
return ToolAnnotations(readOnlyHint=True, destructiveHint=False)
|
||||
return ToolAnnotations(
|
||||
readOnlyHint=False,
|
||||
destructiveHint=name.startswith("delete_")
|
||||
or name in self.destructive_tools,
|
||||
)
|
||||
@@ -29,12 +29,6 @@ class MutingTools(BaseTool):
|
||||
- Managing mute rules (finding-specific muting)
|
||||
"""
|
||||
|
||||
# A wrong mute hides findings silently and for good, so it is flagged like a deletion.
|
||||
# update_mute_rule is included because re-enabling a rule mutes its findings again.
|
||||
destructive_tools = frozenset(
|
||||
{"set_mutelist", "create_mute_rule", "update_mute_rule"}
|
||||
)
|
||||
|
||||
# ===== MUTELIST TOOLS =====
|
||||
|
||||
async def _get_mutelist_raw(self) -> dict[str, Any] | None:
|
||||
|
||||
@@ -34,8 +34,6 @@ class RolesTools(BaseTool):
|
||||
- prowler_set_user_role: Set the role a user holds (idempotent)
|
||||
"""
|
||||
|
||||
destructive_tools = frozenset({"set_user_role"})
|
||||
|
||||
async def list_roles(
|
||||
self,
|
||||
page_size: int = Field(
|
||||
|
||||
@@ -14,7 +14,7 @@ docs_mcp_server = FastMCP("prowler-docs", mask_error_details=True)
|
||||
prowler_docs_search_engine = ProwlerDocsSearchEngine()
|
||||
|
||||
|
||||
@docs_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@docs_mcp_server.tool()
|
||||
def search(
|
||||
term: NonBlankStr = Field(
|
||||
description="The term to search for in the documentation"
|
||||
@@ -41,7 +41,7 @@ def search(
|
||||
return prowler_docs_search_engine.search(term, page_size) # type: ignore In the hint we cannot put SearchResult type because JSON API MCP Generator cannot handle Pydantic models yet
|
||||
|
||||
|
||||
@docs_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@docs_mcp_server.tool()
|
||||
def get_document(
|
||||
doc_path: NonBlankStr = Field(
|
||||
description="Path to the documentation file to retrieve. It is the same as the 'path' field of the search results. Use `prowler_docs_search` to find the path first."
|
||||
|
||||
@@ -167,7 +167,7 @@ def _explain_missing_check_file(
|
||||
|
||||
|
||||
# Security Check Tools
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def list_checks(
|
||||
providers: list[str] = Field(
|
||||
default=[],
|
||||
@@ -251,7 +251,7 @@ async def list_checks(
|
||||
return {"count": len(checks), "checks": checks_list}
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def semantic_search_checks(
|
||||
term: NonBlankStr = Field(
|
||||
description="Search term. Examples: 'public access', 'encryption', 'MFA', 'logging'.",
|
||||
@@ -303,7 +303,7 @@ async def semantic_search_checks(
|
||||
return {"count": len(checks), "checks": checks_list}
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_details(
|
||||
check_id: NonBlankStr = Field(
|
||||
description="The check ID to retrieve details for. Example: 's3_bucket_level_public_access_block'"
|
||||
@@ -441,7 +441,7 @@ async def get_check_details(
|
||||
return result
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_code(
|
||||
provider_id: NonBlankStr = Field(
|
||||
description="Prowler Provider ID. Example: 'aws', 'azure', 'gcp', 'kubernetes'. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
@@ -494,7 +494,7 @@ async def get_check_code(
|
||||
}
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_fixer(
|
||||
provider_id: NonBlankStr = Field(
|
||||
description="Prowler Provider ID. Example: 'aws', 'azure', 'gcp', 'kubernetes'. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
@@ -550,7 +550,7 @@ async def get_check_fixer(
|
||||
|
||||
|
||||
# Compliance Framework Tools
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def list_compliances(
|
||||
provider: list[str] = Field(
|
||||
default=[],
|
||||
@@ -610,7 +610,7 @@ async def list_compliances(
|
||||
return {"count": len(compliances), "compliances": compliances_list}
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def semantic_search_compliances(
|
||||
term: NonBlankStr = Field(
|
||||
description="Search term. Examples: 'CIS', 'HIPAA', 'PCI', 'GDPR', 'SOC2', 'NIST'.",
|
||||
@@ -654,7 +654,7 @@ async def semantic_search_compliances(
|
||||
return {"count": len(compliances), "compliances": compliances_list}
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def get_compliance_details(
|
||||
compliance_id: NonBlankStr = Field(
|
||||
description="The compliance framework ID to retrieve details for. Example: 'cis_4.0_aws'. Use `prowler_hub_list_compliances` or `prowler_hub_semantic_search_compliances` to find available compliance IDs.",
|
||||
@@ -743,7 +743,7 @@ async def get_compliance_details(
|
||||
|
||||
|
||||
# Provider Tools
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def list_providers() -> dict:
|
||||
"""List all providers supported by Prowler.
|
||||
|
||||
@@ -784,7 +784,7 @@ async def list_providers() -> dict:
|
||||
return {"count": len(providers), "providers": providers_list}
|
||||
|
||||
|
||||
@hub_mcp_server.tool(annotations={"readOnlyHint": True, "destructiveHint": False})
|
||||
@hub_mcp_server.tool()
|
||||
async def get_provider_services(
|
||||
provider_id: NonBlankStr = Field(
|
||||
description="The provider ID to get services for. Example: 'aws', 'azure', 'gcp', 'kubernetes'. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
|
||||
@@ -74,14 +74,13 @@ extend-select = [
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
# Transitive pins fastmcp does not raise on its own; each carries a known HIGH.
|
||||
constraint-dependencies = [
|
||||
"authlib==1.9.1",
|
||||
"cryptography==50.0.0",
|
||||
"dnspython==2.9.0",
|
||||
"joserfc==1.6.8",
|
||||
"mcp==1.28.1",
|
||||
"pyjwt==2.14.0",
|
||||
"python-multipart==0.0.30"
|
||||
]
|
||||
package = true
|
||||
@@ -49,36 +49,3 @@ async def test_every_tool_and_parameter_is_described(mcp_root_server):
|
||||
|
||||
for tool in tools:
|
||||
assert_tool_contract(tool)
|
||||
|
||||
|
||||
async def test_every_tool_says_whether_it_is_read_only_or_destructive(mcp_root_server):
|
||||
"""Clients decide whether to ask the user from these hints.
|
||||
|
||||
Left unset, the MCP spec treats a tool as destructive, so a listing would look
|
||||
exactly like a deletion.
|
||||
"""
|
||||
async with Client(mcp_root_server) as client:
|
||||
tools = {tool.name: tool.annotations for tool in await client.list_tools()}
|
||||
|
||||
for name, annotations in tools.items():
|
||||
assert annotations is not None, f"{name} has no annotations"
|
||||
assert annotations.readOnlyHint is not None, f"{name} has no readOnlyHint"
|
||||
assert annotations.destructiveHint is not None, f"{name} has no destructiveHint"
|
||||
|
||||
destructive = {name for name, a in tools.items() if a.destructiveHint}
|
||||
assert destructive == {
|
||||
"prowler_create_mute_rule",
|
||||
"prowler_update_mute_rule",
|
||||
"prowler_set_mutelist",
|
||||
"prowler_delete_mutelist",
|
||||
"prowler_delete_mute_rule",
|
||||
"prowler_delete_provider",
|
||||
"prowler_delete_integration",
|
||||
"prowler_set_user_role",
|
||||
}
|
||||
assert tools["prowler_run_attack_paths_query"].readOnlyHint
|
||||
assert tools["prowler_hub_list_checks"].readOnlyHint
|
||||
assert tools["prowler_docs_search"].readOnlyHint
|
||||
assert tools["prowler_get_mutelist"].readOnlyHint
|
||||
assert tools["prowler_hub_get_check_code"].readOnlyHint
|
||||
assert not tools["prowler_trigger_scan"].readOnlyHint
|
||||
Generated
+9
-11
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 5
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
@@ -10,9 +10,7 @@ resolution-markers = [
|
||||
|
||||
[manifest]
|
||||
constraints = [
|
||||
{ name = "authlib", specifier = "==1.9.1" },
|
||||
{ name = "cryptography", specifier = "==50.0.0" },
|
||||
{ name = "dnspython", specifier = "==2.9.0" },
|
||||
{ name = "joserfc", specifier = "==1.6.8" },
|
||||
{ name = "mcp", specifier = "==1.28.1" },
|
||||
{ name = "pyjwt", specifier = "==2.14.0" },
|
||||
@@ -64,15 +62,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.9.1"
|
||||
version = "1.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "joserfc" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/dc/087c08741a98d234c9b98beef8a28d3c309f814d14b6156422c5815bf6d6/authlib-1.9.1.tar.gz", hash = "sha256:5c7d9848f47cac340f060f76ae0bc09b59f5c8e89cc1bb032d4370a0c55b5974", size = 182700, upload-time = "2026-10-14T14:17:52.307Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/44/a46089266de32a0d809023b6bf31b80a1f26a46954446a7ce695a565cbea/authlib-1.9.1-py2.py3-none-any.whl", hash = "sha256:8b9be8b1e5174dcbf5520e984f1a22dc2edae5b6e40a61d4cedbf86b97e9e69b", size = 266042, upload-time = "2026-10-14T14:17:50.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -352,11 +350,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.9.0"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1295,8 +1293,8 @@ name = "secretstorage"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "jeepney" },
|
||||
{ name = "cryptography", marker = "sys_platform != 'win32'" },
|
||||
{ name = "jeepney", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
|
||||
wheels = [
|
||||
|
||||
@@ -4,23 +4,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [5.47.0] (Prowler v5.47.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `iam_subscription_owners_between_2_and_3` check for Azure provider, verifying each subscription has between 2 and 3 principals with the Owner role, including owners inherited from management groups, mapped to CIS 5.0 and 6.0 [(#13145)](https://github.com/prowler-cloud/prowler/pull/13145)
|
||||
- `network_application_gateway_waf_enabled` and `network_application_gateway_http2_enabled` checks for Azure provider, with Application Gateway and WAF policy collection in the network service, mapped to CIS 5.0 and 6.0 [(#13160)](https://github.com/prowler-cloud/prowler/pull/13160)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Security group internet exposure checks now treat CIDRs in one ingress rule that together cover 0.0.0.0/0 or ::/0 as open to the internet [(#13191)](https://github.com/prowler-cloud/prowler/pull/13191)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- pymongo to 4.18.3, dnspython to 2.9.0 and oauthlib to 4.0.0 in the SDK, patching the BSON decoding and DNS compression pointer denial of service advisories and the OAuth device flow token disclosure; dnspython stays on 2.8.0 for Python 3.10, which 2.9.0 no longer supports [(#13185)](https://github.com/prowler-cloud/prowler/pull/13185)
|
||||
|
||||
---
|
||||
|
||||
## [5.46.0] (Prowler v5.46.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
MongoDB Atlas connection errors now include the HTTP status, error code and detail returned by Atlas instead of a generic HTTP error, and a custom exception message no longer persists into later errors of the same type
|
||||
@@ -1 +0,0 @@
|
||||
Python base image from 3.12.15 to 3.12.16 in the SDK container image, patching CVE-2026-115557
|
||||
@@ -1148,9 +1148,7 @@
|
||||
{
|
||||
"Id": "5.27",
|
||||
"Description": "Ensure there are between 2 and 3 subscription owners",
|
||||
"Checks": [
|
||||
"iam_subscription_owners_between_2_and_3"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "5 Identity Services",
|
||||
@@ -1956,9 +1954,7 @@
|
||||
{
|
||||
"Id": "7.10",
|
||||
"Description": "Ensure Azure Web Application Firewall (WAF) is enabled on Azure Application Gateway",
|
||||
"Checks": [
|
||||
"network_application_gateway_waf_enabled"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "7 Networking Services",
|
||||
@@ -2020,9 +2016,7 @@
|
||||
{
|
||||
"Id": "7.13",
|
||||
"Description": "Ensure 'HTTP2' is set to 'Enabled' on Azure Application Gateway",
|
||||
"Checks": [
|
||||
"network_application_gateway_http2_enabled"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "7 Networking Services",
|
||||
|
||||
@@ -595,9 +595,7 @@
|
||||
{
|
||||
"Id": "5.7",
|
||||
"Description": "Ensure there are between 2 and 3 Subscription Owners",
|
||||
"Checks": [
|
||||
"iam_subscription_owners_between_2_and_3"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "5 Identity Services",
|
||||
@@ -1360,9 +1358,7 @@
|
||||
{
|
||||
"Id": "7.10",
|
||||
"Description": "Ensure Azure Web Application Firewall (WAF) is Enabled on Azure Application Gateway",
|
||||
"Checks": [
|
||||
"network_application_gateway_waf_enabled"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "7 Networking Services",
|
||||
@@ -1424,9 +1420,7 @@
|
||||
{
|
||||
"Id": "7.13",
|
||||
"Description": "Ensure 'HTTP2' is Set to 'Enabled' on Azure Application Gateway",
|
||||
"Checks": [
|
||||
"network_application_gateway_http2_enabled"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "7 Networking Services",
|
||||
|
||||
@@ -52,7 +52,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.48.0"
|
||||
prowler_version = "5.46.1"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -35,7 +35,7 @@ def check_security_group(
|
||||
|
||||
@param ports: List of ports to check. If not provided all ports will be checked unless all_ports is False. (Default: None)
|
||||
|
||||
@param any_address: If True, only 0.0.0.0/0 or "::/0", or CIDRs in the same ingress rule that together cover every address of one IP version, will be public and do not search for public addresses. (Default: False)
|
||||
@param any_address: If True, only 0.0.0.0/0 or "::/0" will be public and do not search for public addresses. (Default: False)
|
||||
|
||||
@param all_ports: If True, empty ports list will be treated as all ports. (Default: False)
|
||||
|
||||
@@ -44,14 +44,8 @@ def check_security_group(
|
||||
if ports is None:
|
||||
ports = []
|
||||
|
||||
# With any_address, CIDRs that together cover every address in one rule, such as
|
||||
# 0.0.0.0/1 and 128.0.0.0/1, are as open as 0.0.0.0/0.
|
||||
rule_covers_all_addresses = any_address and _covers_all_addresses(ingress_rule)
|
||||
|
||||
# Check for all traffic ingress rules regardless of the protocol
|
||||
if ingress_rule["IpProtocol"] == "-1":
|
||||
if rule_covers_all_addresses:
|
||||
return True
|
||||
for ip_ingress_rule in ingress_rule["IpRanges"]:
|
||||
if _is_cidr_public(ip_ingress_rule["CidrIp"], any_address):
|
||||
return True
|
||||
@@ -85,7 +79,7 @@ def check_security_group(
|
||||
|
||||
for ip_ingress_rule in all_ingress_rules:
|
||||
# We only check public CIDRs
|
||||
if rule_covers_all_addresses or _is_cidr_public(
|
||||
if _is_cidr_public(
|
||||
ip_ingress_rule.get("CidrIp", ip_ingress_rule.get("CidrIpv6")),
|
||||
any_address,
|
||||
):
|
||||
@@ -108,27 +102,6 @@ def check_security_group(
|
||||
return False
|
||||
|
||||
|
||||
def _covers_all_addresses(ingress_rule: Any) -> bool:
|
||||
"""
|
||||
Check if the CIDRs of one IP version in the ingress rule together cover the whole address space
|
||||
|
||||
@param ingress_rule: AWS Security Group IpPermissions Ingress Rule
|
||||
"""
|
||||
for key, cidr_key in (("IpRanges", "CidrIp"), ("Ipv6Ranges", "CidrIpv6")):
|
||||
try:
|
||||
networks = [
|
||||
ipaddress.ip_network(ip_range[cidr_key], strict=False)
|
||||
for ip_range in ingress_rule.get(key, [])
|
||||
]
|
||||
except ValueError:
|
||||
continue
|
||||
if any(
|
||||
network.prefixlen == 0 for network in ipaddress.collapse_addresses(networks)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_cidr_public(cidr: str, any_address: bool = False) -> bool:
|
||||
"""
|
||||
Check if an input CIDR is public
|
||||
|
||||
@@ -12,9 +12,6 @@ class IAM(AzureService):
|
||||
def __init__(self, provider: AzureProvider):
|
||||
super().__init__(AuthorizationManagementClient, provider)
|
||||
self.roles, self.custom_roles = self._get_roles()
|
||||
# Subscriptions whose role assignments could not be listed completely,
|
||||
# so an empty or partial result there is not evidence of no assignments.
|
||||
self.role_assignments_lookup_failed: set[str] = set()
|
||||
self.role_assignments = self._get_role_assignments()
|
||||
|
||||
def _get_roles(self):
|
||||
@@ -85,7 +82,6 @@ class IAM(AzureService):
|
||||
}
|
||||
)
|
||||
except Exception as error:
|
||||
self.role_assignments_lookup_failed.add(subscription)
|
||||
logger.error(
|
||||
f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
Whitespace-only changes.
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "iam_subscription_owners_between_2_and_3",
|
||||
"CheckTitle": "Subscription has between 2 and 3 principals assigned the Owner role",
|
||||
"CheckType": [],
|
||||
"ServiceName": "iam",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "microsoft.resources/subscriptions",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "**Azure subscriptions** are evaluated for the number of principals holding the **Owner** role, whether assigned on the subscription or inherited from a management group. Users, groups, service principals and managed identities each count as one principal.",
|
||||
"Risk": "**Too many owners** widen the set of identities that can change any resource, grant access or delete workloads, so one compromised account gains full control.\n\n**A single owner** is a single point of failure: if that account is lost or disabled, nobody can manage access or respond to incidents.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/azure/role-based-access-control/best-practices",
|
||||
"https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#owner",
|
||||
"https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-remove"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az role assignment create --assignee <principal_id_or_upn> --role Owner --scope /subscriptions/<subscription_id>",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Azure portal, open Subscriptions and select the subscription\n2. Go to Access control (IAM) > Role assignments and filter by Role = Owner\n3. If more than 3 principals are listed, select the extra direct assignments (Scope: This resource) and remove them\n4. Remove extra inherited assignments where they are granted: click the link next to (Inherited) in the Scope column and remove the assignment at that management group or root scope\n5. If fewer than 2 are listed, click Add > Add role assignment and assign Owner to a second administrator or group",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Keep **2 to 3 standing owners** per subscription so access can always be recovered without concentrating full control:\n- Grant day-to-day work through narrower roles such as Contributor\n- Use **PIM** for just-in-time Owner elevation\n- Review Owner assignments and group membership regularly",
|
||||
"Url": "https://hub.prowler.com/check/iam_subscription_owners_between_2_and_3"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Counts unique principals with an active Owner assignment on the subscription or inherited from a management group or the root scope, and reports the direct and inherited split. PIM-eligible assignments are not counted."
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.config import OWNER_ROLE_ID
|
||||
from prowler.providers.azure.services.iam.iam_client import iam_client
|
||||
|
||||
MIN_OWNERS = 2
|
||||
MAX_OWNERS = 3
|
||||
|
||||
|
||||
class iam_subscription_owners_between_2_and_3(Check):
|
||||
"""Subscription has between 2 and 3 principals with the Owner role."""
|
||||
|
||||
def execute(self) -> list[Check_Report_Azure]:
|
||||
"""Count the principals that hold the Owner role on each subscription.
|
||||
|
||||
Owners assigned on the subscription and owners inherited from a
|
||||
management group or the root scope all count, once per principal.
|
||||
|
||||
Returns:
|
||||
One finding per subscription. MANUAL when the role assignments of
|
||||
the subscription could not be read completely.
|
||||
"""
|
||||
findings = []
|
||||
for subscription_id, assignments in iam_client.role_assignments.items():
|
||||
subscription_name = iam_client.subscriptions.get(
|
||||
subscription_id, subscription_id
|
||||
)
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource={})
|
||||
report.subscription = subscription_id
|
||||
report.resource_name = subscription_id
|
||||
report.resource_id = f"/subscriptions/{subscription_id}"
|
||||
|
||||
if subscription_id in iam_client.role_assignments_lookup_failed:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = f"Cannot evaluate the owner count of subscription {subscription_name} ({subscription_id}): its role assignments could not be read completely. Verify that Microsoft.Authorization/roleAssignments/read (included in Reader) is granted to the scanning identity."
|
||||
findings.append(report)
|
||||
continue
|
||||
|
||||
subscription_scope = f"/subscriptions/{subscription_id}".casefold()
|
||||
owner_assignments = [
|
||||
assignment
|
||||
for assignment in assignments.values()
|
||||
if assignment.role_id.casefold() == OWNER_ROLE_ID.casefold()
|
||||
]
|
||||
owners = {
|
||||
assignment.agent_id.casefold() for assignment in owner_assignments
|
||||
}
|
||||
direct_owners = {
|
||||
assignment.agent_id.casefold()
|
||||
for assignment in owner_assignments
|
||||
if (assignment.scope or "").casefold() == subscription_scope
|
||||
}
|
||||
owner_count = len(owners)
|
||||
inherited_count = len(owners - direct_owners)
|
||||
breakdown = f"{len(direct_owners)} direct, {inherited_count} inherited"
|
||||
|
||||
if MIN_OWNERS <= owner_count <= MAX_OWNERS:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Subscription {subscription_name} ({subscription_id}) has an owner count of {owner_count} ({breakdown}), within the recommended {MIN_OWNERS} to {MAX_OWNERS}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Subscription {subscription_name} ({subscription_id}) has an owner count of {owner_count} ({breakdown}), outside the recommended {MIN_OWNERS} to {MAX_OWNERS}."
|
||||
findings.append(report)
|
||||
return findings
|
||||
Whitespace-only changes.
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "network_application_gateway_http2_enabled",
|
||||
"CheckTitle": "Application Gateway has HTTP/2 enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "microsoft.network/applicationgateways",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**Azure Application Gateways** are evaluated for **HTTP/2** support on client connections (`enableHttp2`). The gateway offers HTTP/2 only over TLS on HTTPS listeners, and traffic to backend pools always uses HTTP/1.1.",
|
||||
"Risk": "Clients fall back to **HTTP/1.1**, opening more TLS connections and losing HTTP/2 multiplexing and header compression. The direct security impact is low, so treat this as protocol hygiene and performance rather than an exposure.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/azure/application-gateway/configuration-listeners"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az network application-gateway update --resource-group <resource_group> --name <gateway_name> --http2 Enabled",
|
||||
"NativeIaC": "```bicep\nresource appGw 'Microsoft.Network/applicationGateways@2024-05-01' = {\n name: '<example_resource_name>'\n location: '<example_location>'\n properties: {\n enableHttp2: true // Critical: enables HTTP/2 for clients\n // ...existing gateway configuration\n }\n}\n```",
|
||||
"Other": "1. In the Azure portal, open the Application Gateway\n2. Under Settings, select Configuration\n3. Set HTTP2 to Enabled and click Save",
|
||||
"Terraform": "```hcl\nresource \"azurerm_application_gateway\" \"example_resource\" {\n # ...existing gateway configuration\n http2_enabled = true # Critical: enables HTTP/2 for clients\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **HTTP/2** on every gateway so clients that support it negotiate the modern protocol over TLS. HTTP/1.1 clients keep working because the gateway supports both.",
|
||||
"Url": "https://hub.prowler.com/check/network_application_gateway_http2_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "The Azure API leaves HTTP/2 disabled unless it is set, while the Azure portal enables it by default."
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_application_gateway_http2_enabled(Check):
|
||||
"""Application Gateway has HTTP/2 enabled for client connections."""
|
||||
|
||||
def execute(self) -> list[Check_Report_Azure]:
|
||||
"""Evaluate the HTTP/2 setting of every Application Gateway.
|
||||
|
||||
Returns:
|
||||
One finding per Application Gateway, plus one MANUAL finding for
|
||||
each subscription whose gateways could not be listed completely.
|
||||
"""
|
||||
findings = []
|
||||
for subscription_id, gateways in network_client.application_gateways.items():
|
||||
subscription_name = network_client.subscriptions.get(
|
||||
subscription_id, subscription_id
|
||||
)
|
||||
if subscription_id in network_client.application_gateways_lookup_failed:
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource={})
|
||||
report.subscription = subscription_id
|
||||
report.resource_name = subscription_id
|
||||
report.resource_id = f"/subscriptions/{subscription_id}"
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = f"Cannot evaluate HTTP/2 on the Application Gateways in subscription {subscription_name} ({subscription_id}): they could not be listed completely. Check the scan log for the error, and verify that Microsoft.Network/applicationGateways/read (included in Reader) is granted to the scanning identity."
|
||||
findings.append(report)
|
||||
for gateway in gateways.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=gateway)
|
||||
report.subscription = subscription_id
|
||||
prefix = f"Application Gateway {gateway.name} in subscription {subscription_name} ({subscription_id})"
|
||||
if gateway.http2_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{prefix} has HTTP/2 enabled."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{prefix} does not have HTTP/2 enabled."
|
||||
findings.append(report)
|
||||
return findings
|
||||
Whitespace-only changes.
-37
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "network_application_gateway_waf_enabled",
|
||||
"CheckTitle": "Application Gateway has an enabled Web Application Firewall policy associated",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "microsoft.network/applicationgateways",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**Azure Application Gateways** are evaluated for an enabled **Web Application Firewall (WAF) policy** associated at the gateway level, which requires the `WAF_v2` tier. The legacy WAF configuration does not count as a policy.",
|
||||
"Risk": "Without a WAF, web applications behind the gateway receive **SQL injection**, **cross-site scripting** and other common exploits unfiltered, risking **data theft**, **account takeover** and **defacement** of internet-facing services.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/azure/web-application-firewall/ag/ag-overview",
|
||||
"https://learn.microsoft.com/en-us/azure/web-application-firewall/ag/upgrade-ag-waf-policy"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az network application-gateway update --resource-group <resource_group> --name <gateway_name> --set sku.name=WAF_v2 sku.tier=WAF_v2 firewallPolicy.id=<waf_policy_id> forceFirewallPolicyAssociation=true",
|
||||
"NativeIaC": "```bicep\nresource appGw 'Microsoft.Network/applicationGateways@2024-05-01' = {\n name: '<example_resource_name>'\n location: '<example_location>'\n properties: {\n sku: { name: 'WAF_v2', tier: 'WAF_v2' } // Critical: WAF tier\n firewallPolicy: { id: '<example_resource_id>' } // Critical: associates the WAF policy\n forceFirewallPolicyAssociation: true // Replaces a legacy WAF configuration with the policy\n // ...existing gateway configuration, without webApplicationFirewallConfiguration\n }\n}\n```",
|
||||
"Other": "1. In the Azure portal, open the Application Gateway\n2. If the tier is Standard V2, select Configuration under Settings, set Tier to WAF V2 and click Save. A V1 gateway must first be migrated to V2, and a Basic gateway cannot change tier and must be redeployed on WAF V2\n3. Select Web Application Firewall. If the gateway uses the legacy WAF configuration, select Upgrade from WAF configuration, name the new policy and click Upgrade. This copies the legacy exclusions and rule settings into the policy, whereas the CLI and IaC examples force the association and apply the policy's own settings instead\n4. Otherwise associate a new or existing WAF policy and click Save\n5. If the associated policy is disabled, enable it, for example with `az network application-gateway waf-policy policy-setting update --resource-group <waf_policy_resource_group> --policy-name <waf_policy_name> --state Enabled --request-body-check true`. Keep `--request-body-check true`: without it, this command turns request body inspection off",
|
||||
"Terraform": "```hcl\nresource \"azurerm_application_gateway\" \"example_resource\" {\n # ...existing gateway configuration, without a waf_configuration block\n sku {\n name = \"WAF_v2\" # Critical: WAF tier\n tier = \"WAF_v2\"\n }\n firewall_policy_id = azurerm_web_application_firewall_policy.example_resource.id # Critical: associates the WAF policy\n force_firewall_policy_association = true # Replaces a legacy WAF configuration with the policy\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Put an **enabled WAF policy in Prevention mode** in front of every internet-facing gateway, using the latest Default Rule Set. Migrate gateways that still use the legacy WAF configuration to a policy before it retires.",
|
||||
"Url": "https://hub.prowler.com/check/network_application_gateway_waf_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Gateways using the legacy WAF configuration are reported as FAIL because Microsoft retires it on 15 March 2027 and newer WAF features, such as bot protection, require a policy. Only the gateway-level policy is evaluated, as in the CIS audit; policies attached to listeners or path rules are not. The policy state comes from the WAF policies collected in the same scan, or is read from the policy itself when it sits outside the scanned resource groups, and a disabled policy is reported as FAIL. The policy mode is not evaluated, so an enabled policy in Detection mode, which only logs requests, is reported as PASS."
|
||||
}
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_application_gateway_waf_enabled(Check):
|
||||
"""Application Gateway has an enabled Web Application Firewall policy associated."""
|
||||
|
||||
def execute(self) -> list[Check_Report_Azure]:
|
||||
"""Evaluate the WAF policy association of every Application Gateway.
|
||||
|
||||
Returns:
|
||||
One finding per Application Gateway, plus one MANUAL finding for
|
||||
each subscription whose gateways, or whose WAF policies with a
|
||||
gateway's policy state unknown, could not be listed or read
|
||||
completely. A gateway covered by that finding gets none of its
|
||||
own. Any other gateway whose policy state cannot be determined is
|
||||
MANUAL.
|
||||
"""
|
||||
findings = []
|
||||
for subscription_id, gateways in network_client.application_gateways.items():
|
||||
subscription_name = network_client.subscriptions.get(
|
||||
subscription_id, subscription_id
|
||||
)
|
||||
gateways_unlisted = (
|
||||
subscription_id in network_client.application_gateways_lookup_failed
|
||||
)
|
||||
# A policy state left unknown by a failed listing or read is reported
|
||||
# once for the subscription; a missing policy keeps its own finding.
|
||||
policies_unlisted = (
|
||||
subscription_id in network_client.waf_policies_lookup_failed
|
||||
)
|
||||
unknown_policy_state = {
|
||||
gateway.id
|
||||
for gateway in gateways.values()
|
||||
if gateway.firewall_policy_id
|
||||
and gateway.firewall_policy_enabled is None
|
||||
and (
|
||||
policies_unlisted
|
||||
or gateway.firewall_policy_id.casefold()
|
||||
in network_client.waf_policy_reads_failed
|
||||
)
|
||||
}
|
||||
policies_unreadable = bool(unknown_policy_state)
|
||||
if gateways_unlisted or policies_unreadable:
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource={})
|
||||
report.subscription = subscription_id
|
||||
report.resource_name = subscription_id
|
||||
report.resource_id = f"/subscriptions/{subscription_id}"
|
||||
report.status = "MANUAL"
|
||||
scope = f"Cannot evaluate the WAF policy association of the Application Gateways in subscription {subscription_name} ({subscription_id})"
|
||||
if gateways_unlisted and policies_unreadable:
|
||||
report.status_extended = f"{scope}: they could not be listed completely, and the WAF policies associated with them could not be listed or read completely. Check the scan log for the errors, and verify that Microsoft.Network/applicationGateways/read and Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/read (both included in Reader) are granted to the scanning identity."
|
||||
elif gateways_unlisted:
|
||||
report.status_extended = f"{scope}: they could not be listed completely. Check the scan log for the error, and verify that Microsoft.Network/applicationGateways/read (included in Reader) is granted to the scanning identity."
|
||||
else:
|
||||
report.status_extended = f"{scope}: the WAF policies associated with them could not be listed or read completely. Check the scan log for the error, and verify that Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/read (included in Reader) is granted to the scanning identity."
|
||||
findings.append(report)
|
||||
for gateway in gateways.values():
|
||||
if policies_unreadable and gateway.id in unknown_policy_state:
|
||||
continue
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=gateway)
|
||||
report.subscription = subscription_id
|
||||
prefix = f"Application Gateway {gateway.name} in subscription {subscription_name} ({subscription_id})"
|
||||
if gateway.firewall_policy_id:
|
||||
if gateway.firewall_policy_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"{prefix} has an enabled WAF policy associated."
|
||||
)
|
||||
elif gateway.firewall_policy_enabled is False:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{prefix} has a WAF policy associated, but the policy is disabled."
|
||||
else:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = f"{prefix} has a WAF policy associated, but the state of that policy could not be determined. Check the scan log for the error."
|
||||
elif gateway.waf_configuration_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{prefix} uses the legacy WAF configuration instead of a WAF policy."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"{prefix} has no gateway-level WAF policy associated."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -1,26 +1,14 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Iterable, List, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from azure.mgmt.network import NetworkManagementClient
|
||||
from azure.mgmt.network import models as network_models
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.azure.lib.service.service import AzureService
|
||||
|
||||
WAF_POLICY_ID = re.compile(
|
||||
r"^/subscriptions/(?P<subscription>[^/]+)/resourceGroups/(?P<resource_group>[^/]+)"
|
||||
r"/providers/Microsoft\.Network/ApplicationGatewayWebApplicationFirewallPolicies"
|
||||
r"/(?P<name>[^/]+)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# Whether a WAF policy in each state, matched without regard to case, is enabled.
|
||||
WAF_POLICY_STATES = {"enabled": True, "disabled": False}
|
||||
|
||||
|
||||
class Network(AzureService):
|
||||
def __init__(self, provider: AzureProvider):
|
||||
@@ -30,14 +18,6 @@ class Network(AzureService):
|
||||
self.network_watchers = self._get_network_watchers()
|
||||
self.public_ip_addresses = self._get_public_ip_addresses()
|
||||
self.virtual_networks = self._get_virtual_networks()
|
||||
# Subscriptions whose WAF policies could not be listed completely.
|
||||
self.waf_policies_lookup_failed: set[str] = set()
|
||||
self.waf_policies = self._get_waf_policies()
|
||||
# Casefolded IDs of WAF policies outside that listing that could not be read.
|
||||
self.waf_policy_reads_failed: set[str] = set()
|
||||
# Subscriptions whose Application Gateways could not be listed completely.
|
||||
self.application_gateways_lookup_failed: set[str] = set()
|
||||
self.application_gateways = self._get_application_gateways()
|
||||
|
||||
def _get_security_groups(self):
|
||||
logger.info("Network - Getting Network Security Groups...")
|
||||
@@ -278,109 +258,6 @@ class Network(AzureService):
|
||||
)
|
||||
return virtual_networks
|
||||
|
||||
def _get_waf_policies(self) -> dict:
|
||||
"""Get the Application Gateway WAF policies of every subscription.
|
||||
|
||||
A subscription whose WAF policies cannot be listed or mapped completely
|
||||
is added to waf_policies_lookup_failed. A policy that cannot be mapped
|
||||
is logged and skipped, so the others are still collected.
|
||||
|
||||
Returns:
|
||||
A dictionary keyed by subscription ID whose values map each WAF
|
||||
policy ID to a WafPolicy model.
|
||||
"""
|
||||
logger.info("Network - Getting Application Gateway WAF policies...")
|
||||
waf_policies = {}
|
||||
for subscription_id, client in self.clients.items():
|
||||
try:
|
||||
waf_policies[subscription_id] = {}
|
||||
waf_policies_list = self.list_with_rg_scope(
|
||||
subscription_id,
|
||||
client.web_application_firewall_policies.list_all,
|
||||
mark_failed_lookups(
|
||||
client.web_application_firewall_policies.list,
|
||||
self.waf_policies_lookup_failed,
|
||||
subscription_id,
|
||||
),
|
||||
)
|
||||
for waf_policy in waf_policies_list:
|
||||
try:
|
||||
policy = build_waf_policy(waf_policy)
|
||||
waf_policies[subscription_id][policy.id] = policy
|
||||
except Exception as error:
|
||||
self.waf_policies_lookup_failed.add(subscription_id)
|
||||
logger.error(
|
||||
f"Subscription ID: {subscription_id} -- WAF policy: {getattr(waf_policy, 'id', None)} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
self.waf_policies_lookup_failed.add(subscription_id)
|
||||
logger.error(
|
||||
f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return waf_policies
|
||||
|
||||
def _get_application_gateways(self) -> dict:
|
||||
"""Get the Application Gateways of every subscription.
|
||||
|
||||
The state of each gateway's WAF policy comes from the policies that
|
||||
_get_waf_policies collected, so that method must run first, and a
|
||||
policy that was not listed is read once by its ID. A subscription whose
|
||||
gateways cannot be listed or mapped completely is added to
|
||||
application_gateways_lookup_failed. A gateway that cannot be mapped is
|
||||
logged and skipped, so the others are still collected.
|
||||
|
||||
Returns:
|
||||
A dictionary keyed by subscription ID whose values map each
|
||||
Application Gateway ID to an ApplicationGateway model.
|
||||
"""
|
||||
logger.info("Network - Getting Application Gateways...")
|
||||
application_gateways = {}
|
||||
for subscription_id, client in self.clients.items():
|
||||
try:
|
||||
application_gateways[subscription_id] = {}
|
||||
application_gateways_list = self.list_with_rg_scope(
|
||||
subscription_id,
|
||||
client.application_gateways.list_all,
|
||||
mark_failed_lookups(
|
||||
client.application_gateways.list,
|
||||
self.application_gateways_lookup_failed,
|
||||
subscription_id,
|
||||
),
|
||||
)
|
||||
listed_policies = {
|
||||
policy_id.casefold(): policy
|
||||
for policy_id, policy in self.waf_policies.get(
|
||||
subscription_id, {}
|
||||
).items()
|
||||
}
|
||||
policy_states = {}
|
||||
for application_gateway in application_gateways_list:
|
||||
try:
|
||||
gateway = build_application_gateway(application_gateway)
|
||||
if gateway.firewall_policy_id:
|
||||
key = gateway.firewall_policy_id.casefold()
|
||||
if key not in policy_states:
|
||||
policy_states[key] = get_waf_policy_enabled(
|
||||
client,
|
||||
subscription_id,
|
||||
gateway.firewall_policy_id,
|
||||
listed_policies,
|
||||
self.waf_policy_reads_failed,
|
||||
)
|
||||
gateway.firewall_policy_enabled = policy_states[key]
|
||||
application_gateways[subscription_id][gateway.id] = gateway
|
||||
except Exception as error:
|
||||
self.application_gateways_lookup_failed.add(subscription_id)
|
||||
logger.error(
|
||||
f"Subscription ID: {subscription_id} -- Application Gateway: {getattr(application_gateway, 'id', None)} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
self.application_gateways_lookup_failed.add(subscription_id)
|
||||
logger.error(
|
||||
f"Subscription ID: {subscription_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return application_gateways
|
||||
|
||||
|
||||
@dataclass
|
||||
class BastionHost:
|
||||
@@ -459,192 +336,3 @@ class VirtualNetwork:
|
||||
def __post_init__(self):
|
||||
if self.subnets is None:
|
||||
self.subnets = []
|
||||
|
||||
|
||||
class ApplicationGateway(BaseModel):
|
||||
"""Application Gateway settings used by the network checks."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
firewall_policy_id: Optional[str] = None
|
||||
firewall_policy_enabled: Optional[bool] = None
|
||||
waf_configuration_enabled: bool = False
|
||||
http2_enabled: bool = False
|
||||
|
||||
|
||||
class WafPolicy(BaseModel):
|
||||
"""Application Gateway WAF policy settings used by the network checks."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
state: Optional[str] = None
|
||||
|
||||
|
||||
def mark_failed_lookups(
|
||||
list_by_rg_fn: Callable[..., Iterable[object]],
|
||||
lookup_failed: set[str],
|
||||
subscription_id: str,
|
||||
) -> Callable[..., List[object]]:
|
||||
"""Wrap a per-resource-group list call so that a failure marks the subscription.
|
||||
|
||||
list_with_rg_scope logs and skips a resource group whose list call fails,
|
||||
so the subscription is recorded here before the error reaches it. A
|
||||
resource group that no longer exists (not found) is not a failed lookup.
|
||||
|
||||
Args:
|
||||
list_by_rg_fn: Azure SDK list operation for one resource group.
|
||||
lookup_failed: Set of subscriptions whose listing was incomplete.
|
||||
subscription_id: Subscription the list call belongs to.
|
||||
|
||||
Returns:
|
||||
A callable with the same keyword arguments as list_by_rg_fn.
|
||||
"""
|
||||
|
||||
def list_by_resource_group(**kwargs) -> List[object]:
|
||||
try:
|
||||
return list(list_by_rg_fn(**kwargs))
|
||||
except ResourceNotFoundError:
|
||||
raise
|
||||
except Exception:
|
||||
lookup_failed.add(subscription_id)
|
||||
raise
|
||||
|
||||
return list_by_resource_group
|
||||
|
||||
|
||||
def build_application_gateway(
|
||||
application_gateway: network_models.ApplicationGateway,
|
||||
) -> ApplicationGateway:
|
||||
"""Convert an Azure SDK Application Gateway into an ApplicationGateway model.
|
||||
|
||||
The state of the associated WAF policy is not part of the gateway, so
|
||||
firewall_policy_enabled is set afterwards from get_waf_policy_enabled.
|
||||
|
||||
Args:
|
||||
application_gateway: ApplicationGateway from the Azure SDK.
|
||||
|
||||
Returns:
|
||||
The ApplicationGateway model.
|
||||
"""
|
||||
waf_configuration = getattr(
|
||||
application_gateway, "web_application_firewall_configuration", None
|
||||
)
|
||||
return ApplicationGateway(
|
||||
id=application_gateway.id,
|
||||
name=application_gateway.name,
|
||||
location=application_gateway.location,
|
||||
firewall_policy_id=getattr(
|
||||
getattr(application_gateway, "firewall_policy", None), "id", None
|
||||
),
|
||||
waf_configuration_enabled=bool(getattr(waf_configuration, "enabled", False)),
|
||||
http2_enabled=bool(getattr(application_gateway, "enable_http2", False)),
|
||||
)
|
||||
|
||||
|
||||
def build_waf_policy(
|
||||
waf_policy: network_models.WebApplicationFirewallPolicy,
|
||||
) -> WafPolicy:
|
||||
"""Convert an Azure SDK WAF policy into a WafPolicy model.
|
||||
|
||||
Args:
|
||||
waf_policy: WebApplicationFirewallPolicy from the Azure SDK.
|
||||
|
||||
Returns:
|
||||
The WafPolicy model.
|
||||
"""
|
||||
settings = getattr(waf_policy, "policy_settings", None)
|
||||
return WafPolicy(
|
||||
id=waf_policy.id,
|
||||
name=waf_policy.name,
|
||||
location=waf_policy.location,
|
||||
state=getattr(settings, "state", None),
|
||||
)
|
||||
|
||||
|
||||
def read_waf_policy(
|
||||
client: NetworkManagementClient,
|
||||
subscription_id: str,
|
||||
policy_id: str,
|
||||
reads_failed: set[str],
|
||||
) -> Optional[WafPolicy]:
|
||||
"""Read one Application Gateway WAF policy by its resource ID.
|
||||
|
||||
A read that fails for any reason other than the policy not existing adds
|
||||
the casefolded policy ID to reads_failed. Every failure is logged.
|
||||
|
||||
Args:
|
||||
client: NetworkManagementClient of the gateway's subscription.
|
||||
subscription_id: Subscription of the gateway.
|
||||
policy_id: Resource ID of the WAF policy.
|
||||
reads_failed: Casefolded IDs of WAF policies that could not be read.
|
||||
|
||||
Returns:
|
||||
The WafPolicy model, or None when the ID is not a WAF policy ID in the
|
||||
subscription or the policy cannot be read.
|
||||
"""
|
||||
match = WAF_POLICY_ID.match(policy_id)
|
||||
if (
|
||||
not match
|
||||
or match.group("subscription").casefold() != subscription_id.casefold()
|
||||
):
|
||||
logger.warning(
|
||||
f"Subscription ID: {subscription_id} -- WAF policy: {policy_id} -- not an Application Gateway WAF policy ID in this subscription, so the policy was not read."
|
||||
)
|
||||
return None
|
||||
try:
|
||||
return build_waf_policy(
|
||||
client.web_application_firewall_policies.get(
|
||||
resource_group_name=match.group("resource_group"),
|
||||
policy_name=match.group("name"),
|
||||
)
|
||||
)
|
||||
except ResourceNotFoundError as error:
|
||||
logger.warning(
|
||||
f"Subscription ID: {subscription_id} -- WAF policy: {policy_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
reads_failed.add(policy_id.casefold())
|
||||
logger.error(
|
||||
f"Subscription ID: {subscription_id} -- WAF policy: {policy_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_waf_policy_enabled(
|
||||
client: NetworkManagementClient,
|
||||
subscription_id: str,
|
||||
policy_id: str,
|
||||
listed_policies: dict,
|
||||
reads_failed: set[str],
|
||||
) -> Optional[bool]:
|
||||
"""Return whether the WAF policy associated with a gateway is enabled.
|
||||
|
||||
The policy is looked up among the WAF policies listed for the
|
||||
subscription, and read by its ID only when it was not listed, such as a
|
||||
policy outside the scanned resource groups.
|
||||
|
||||
Args:
|
||||
client: NetworkManagementClient of the gateway's subscription.
|
||||
subscription_id: Subscription of the gateway.
|
||||
policy_id: Resource ID of the associated WAF policy.
|
||||
listed_policies: WafPolicy models listed for the subscription, keyed by
|
||||
casefolded policy ID.
|
||||
reads_failed: Casefolded IDs of WAF policies that could not be read.
|
||||
|
||||
Returns:
|
||||
True when the policy is enabled, False when it is disabled, and None
|
||||
when its state cannot be determined.
|
||||
"""
|
||||
policy = listed_policies.get(policy_id.casefold())
|
||||
if policy is None:
|
||||
policy = read_waf_policy(client, subscription_id, policy_id, reads_failed)
|
||||
if policy is None:
|
||||
return None
|
||||
enabled = WAF_POLICY_STATES.get((policy.state or "").casefold())
|
||||
if enabled is None:
|
||||
logger.warning(
|
||||
f"Subscription ID: {subscription_id} -- WAF policy: {policy_id} -- unrecognised policy state: {policy.state}"
|
||||
)
|
||||
return enabled
|
||||
@@ -38,9 +38,7 @@ class MongoDBAtlasBaseException(ProwlerException):
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
provider = "MongoDB Atlas"
|
||||
error_info = dict(
|
||||
self.MONGODBATLAS_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
)
|
||||
error_info = self.MONGODBATLAS_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
|
||||
@@ -236,12 +236,7 @@ class MongodbatlasProvider(Provider):
|
||||
message="MongoDB Atlas authentication failed. Please check your API credentials.",
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
raise MongoDBAtlasIdentityError(
|
||||
file=os.path.basename(__file__),
|
||||
message=MongodbatlasProvider._describe_api_error(response),
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
organizations_data = response.json()
|
||||
|
||||
# Extract organization information from the response
|
||||
@@ -271,7 +266,7 @@ class MongodbatlasProvider(Provider):
|
||||
|
||||
return identity
|
||||
|
||||
except (MongoDBAtlasAuthenticationError, MongoDBAtlasIdentityError):
|
||||
except MongoDBAtlasAuthenticationError:
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
@@ -281,22 +276,6 @@ class MongodbatlasProvider(Provider):
|
||||
original_exception=error,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _describe_api_error(response) -> str:
|
||||
"""Build an identity error message that keeps the reason Atlas returns."""
|
||||
message = f"MongoDB Atlas identity setup failed - HTTP {response.status_code}"
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
return message
|
||||
if not isinstance(body, dict):
|
||||
return message
|
||||
if body.get("errorCode"):
|
||||
message += f" {body['errorCode']}"
|
||||
if body.get("detail"):
|
||||
message += f": {body['detail']}"
|
||||
return message
|
||||
|
||||
def print_credentials(self):
|
||||
"""Print the MongoDB Atlas credentials"""
|
||||
report_lines = [
|
||||
|
||||
+4
-5
@@ -145,7 +145,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
version = "5.48.0"
|
||||
version = "5.46.1"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
@@ -242,8 +242,7 @@ constraint-dependencies = [
|
||||
"deprecated==1.3.1",
|
||||
"dill==0.4.1",
|
||||
"distro==1.9.0",
|
||||
"dnspython==2.8.0; python_version < '3.11'",
|
||||
"dnspython==2.9.0; python_version >= '3.11'",
|
||||
"dnspython==2.8.0",
|
||||
"docker==7.1.0",
|
||||
"dogpile-cache==1.5.0",
|
||||
"durationpy==0.10",
|
||||
@@ -321,7 +320,7 @@ constraint-dependencies = [
|
||||
"narwhals==2.21.0",
|
||||
"nest-asyncio==1.6.0",
|
||||
"networkx==3.4.2",
|
||||
"oauthlib==4.0.0",
|
||||
"oauthlib==3.3.1",
|
||||
"openapi-schema-validator==0.6.3",
|
||||
"openapi-spec-validator==0.7.1",
|
||||
"opentelemetry-api==1.41.1",
|
||||
@@ -405,7 +404,7 @@ override-dependencies = [
|
||||
# huaweicloudsdkcore pins pymongo==4.15.1 exactly (still so in 3.1.216) and only uses
|
||||
# its bson types. The container installs from this lock, so the override reaches the
|
||||
# image; a pip install of the wheel still resolves 4.15.1 through the huawei pin.
|
||||
"pymongo==4.18.3"
|
||||
"pymongo==4.18.2"
|
||||
]
|
||||
|
||||
[tool.vulture]
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: prowler-tour
|
||||
description: >
|
||||
Keeps product-tour definitions aligned with the UI features they describe.
|
||||
Trigger: When modifying UI components that have associated tours, editing tour
|
||||
definition files, or renaming data-tour-id attributes.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "1.0"
|
||||
scope: [root, ui]
|
||||
auto_invoke:
|
||||
- "Editing a UI file containing data-tour-id attributes"
|
||||
- "Adding, updating, or removing a tour definition (*.tour.ts)"
|
||||
- "Renaming or removing a data-tour-id attribute value"
|
||||
- "Changing button labels or section headings on a tour-covered page"
|
||||
- "Restructuring routes or layouts covered by a tour"
|
||||
allowed-tools: Read, Glob, Grep
|
||||
---
|
||||
|
||||
# prowler-tour
|
||||
|
||||
**Report-only.** This skill never edits tour files or UI files; it inspects
|
||||
the change, reports drift it finds between tours and the covered UI, and
|
||||
recommends actions for the developer to apply.
|
||||
|
||||
## Early-exit rule
|
||||
|
||||
Run this check first. Most UI edits are not tour-related — exit cheaply.
|
||||
|
||||
1. Glob `ui/lib/tours/*.tour.ts`.
|
||||
2. For each tour, check whether any `coversFiles` glob pattern matches any
|
||||
file in the current change.
|
||||
3. If no tour matches, respond **exactly**:
|
||||
|
||||
> No tour affected — skipping alignment check
|
||||
|
||||
and exit. Do not proceed to the checklist.
|
||||
4. If at least one tour matches, continue to "Drift checklist" for that tour.
|
||||
|
||||
## Drift checklist
|
||||
|
||||
For each affected tour, evaluate every item. Skip items that obviously do
|
||||
not apply, but list explicitly which items were checked.
|
||||
|
||||
1. **Orphan selectors** — every step's `target` (which composes to
|
||||
`data-tour-id="<tour-id>-<step.target>"`) must resolve to a real element
|
||||
in the codebase. Grep `ui/` for the expected attribute value; report
|
||||
any step whose target is missing.
|
||||
2. **Renamed selectors** — a `data-tour-id` attribute was edited in this
|
||||
change. Match it back to any tour step referencing the old value.
|
||||
3. **Outdated copy** — a popover `title`/`description` references a button
|
||||
label, heading, or term that no longer exists on the covered page.
|
||||
4. **Obsolete steps** — a step describes a section, panel, or workflow
|
||||
that was removed.
|
||||
5. **Missing steps** — a new feature was added on the covered surface
|
||||
without a corresponding step (e.g. a new panel, a new primary action,
|
||||
a new wizard stage).
|
||||
6. **Reordered flow** — the user's path through the feature changed (e.g.
|
||||
query builder moved before scan selection) and the step order no
|
||||
longer reflects it.
|
||||
|
||||
## Version-bump decision tree
|
||||
|
||||
Apply per tour after listing drift:
|
||||
|
||||
- **NO bump** when the change is cosmetic. Examples: fix a typo, soften
|
||||
copy, rename a `data-tour-id` selector while keeping the same step,
|
||||
swap one screenshot for another, tighten wording.
|
||||
- **BUMP `version`** when the user-visible flow changes materially.
|
||||
Examples: a new step was added or removed; the order changed; an
|
||||
anchored target was retargeted to a different panel; the tour now
|
||||
covers a new feature on the surface.
|
||||
|
||||
When in doubt, ask: "Would a user who already saw the previous version
|
||||
miss something useful by not seeing this one?" If yes, bump.
|
||||
|
||||
## Output format
|
||||
|
||||
When emitting a report, follow the exact structure in
|
||||
`references/output-format.md`. The structure is mandatory because the
|
||||
report is consumed downstream and tolerates no field reordering.
|
||||
|
||||
## What this skill MUST NOT do
|
||||
|
||||
- Do not edit `*.tour.ts` files. This skill is report-only.
|
||||
- Do not edit UI files to add or rename `data-tour-id` attributes.
|
||||
- Do not invent new tours. Authoring a new tour is a separate, deliberate
|
||||
decision — the developer makes it, not the skill.
|
||||
- Do not flag drift in tours whose `coversFiles` do not match any file
|
||||
in the current change. Stick to the early-exit rule.
|
||||
|
||||
## See also
|
||||
|
||||
- `references/output-format.md` — exact report template (read when
|
||||
emitting a report).
|
||||
- `references/tours-architecture.md` — code map for the tour abstraction
|
||||
under `ui/lib/tours/`.
|
||||
- `assets/tour-template.ts` — boilerplate for authoring a new `*.tour.ts`.
|
||||
@@ -0,0 +1,51 @@
|
||||
// @ts-nocheck -- template only; resolves once copied into `ui/lib/tours/`
|
||||
/**
|
||||
* Tour template — copy this file to `ui/lib/tours/<your-id>.tour.ts` and
|
||||
* fill in the placeholders. See `references/tours-architecture.md` for the
|
||||
* design context.
|
||||
*
|
||||
* Conventions:
|
||||
* - Declare via `defineTour({...})` (NOT `: TourDefinition`) so TS
|
||||
* preserves the literal union of `target` values. `useDriverTour` uses
|
||||
* that union to validate `stepHandlers` keys and `waitForStep` args.
|
||||
* - `id` is kebab-case and unique across all tours.
|
||||
* - Anchored steps reference DOM via `data-tour-id="<id>-<step.target>"`;
|
||||
* the hook composes the CSS selector automatically.
|
||||
* - `coversFiles` lists the globs that describe the tour's surface; the
|
||||
* `prowler-tour` skill consumes this to decide whether to evaluate
|
||||
* drift on a given change.
|
||||
* - Material flow changes bump `version`; cosmetic edits do not.
|
||||
*/
|
||||
import {
|
||||
defineTour,
|
||||
TOUR_STEP_ALIGNMENTS,
|
||||
TOUR_STEP_SIDES,
|
||||
} from "@/lib/tours/tour-types";
|
||||
|
||||
export const yourTour = defineTour({
|
||||
id: "your-tour-id",
|
||||
version: 1,
|
||||
coversFiles: [
|
||||
// List the UI files this tour describes, using globs under `ui/`.
|
||||
// Example: "ui/app/(prowler)/your-feature/**"
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
// Modal step — no anchor. Use for intros, outros, and any step
|
||||
// that does not point at a specific DOM element.
|
||||
title: "Welcome",
|
||||
description: "Short, plain-English description.",
|
||||
},
|
||||
{
|
||||
// Anchored step. The hook resolves
|
||||
// `[data-tour-id="your-tour-id-step-name"]` lazily, so the element
|
||||
// can be conditionally rendered as long as it exists when the step
|
||||
// becomes active.
|
||||
target: "step-name",
|
||||
side: TOUR_STEP_SIDES.BOTTOM,
|
||||
align: TOUR_STEP_ALIGNMENTS.START,
|
||||
title: "Where the action is",
|
||||
description: "Tell the user what to look at here and why.",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
# Tour Alignment Report — output format
|
||||
|
||||
The report is consumed downstream. Field names, order, and headings are
|
||||
load-bearing — do not rename, reorder, or omit them.
|
||||
|
||||
## Template
|
||||
|
||||
```text
|
||||
## Tour Alignment Report
|
||||
**Tour:** `<tour-id>@v<version>`
|
||||
**Files touched:** <comma-separated list of files in the change>
|
||||
|
||||
### Drift detected
|
||||
- <one bullet per drift item; include file:line where available>
|
||||
|
||||
### Recommended actions
|
||||
1. <numbered, actionable steps the developer should take>
|
||||
|
||||
### Version bump verdict
|
||||
- <BUMP | NO bump> — <one-line rationale>
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- One report per affected tour. If multiple tours are affected, separate
|
||||
reports with a `---` line.
|
||||
- If no drift is detected for an affected tour, still emit the report:
|
||||
put "No drift detected." under "Drift detected" and "None required."
|
||||
under "Recommended actions". The verdict line is still mandatory.
|
||||
- The verdict is exactly one of `BUMP` or `NO bump` — see the
|
||||
version-bump decision tree in `SKILL.md`.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Tours Architecture
|
||||
|
||||
The product-tour abstraction lives under [`ui/lib/tours/`](../../../ui/lib/tours/).
|
||||
This skill operates on tour definitions that follow this architecture.
|
||||
|
||||
## Code map
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `ui/lib/tours/tour-types.ts` | Public type surface: `TourDefinition`, `TourStep`, `TourId`, `TourCompletionRecord`, completion-state const map. Also exports `defineTour(...)` — the required authoring helper that preserves literal step `target`s so `useDriverTour` can type-check `stepHandlers` keys and `waitForStep` arguments. |
|
||||
| `ui/lib/tours/tour-config.ts` | `baseDriverConfig`, `getDriverConfig(theme, overrides?)`, overlay-color map. |
|
||||
| `ui/lib/tours/store/tour-completion-store.ts` | Persistence interface — the swap point for future API adapters. |
|
||||
| `ui/lib/tours/store/local-storage-adapter.ts` | The only adapter in the PoC. Key format: `prowler.tour.<id>.v<version>`. |
|
||||
| `ui/lib/tours/use-driver-tour.ts` | React hook. Initializes driver.js, derives `overlayColor` from `useTheme()`, persists completion. |
|
||||
| `ui/lib/tours/<id>.tour.ts` | One file per tour. Declared via `defineTour({...})` (not `: TourDefinition`) and imported by the page that opts the user in. |
|
||||
| `ui/styles/tours.css` | `.driver-popover.prowler-theme` — every color resolved via `var(--...)` from `globals.css`. |
|
||||
|
||||
## Selector convention
|
||||
|
||||
Tour steps anchor via `data-tour-id="<tour-id>-<step.target>"`. The hook
|
||||
composes the CSS selector at runtime; tour authors only provide the step
|
||||
name in `step.target`. Class-based, ID-based, structural selectors are
|
||||
forbidden — they couple tours to styling decisions that legitimately
|
||||
change.
|
||||
|
||||
## Identity and versioning
|
||||
|
||||
A tour is `{ id, version }`. The localStorage key composes both. A
|
||||
**material content change** bumps `version`; cosmetic edits do not. The
|
||||
decision tree lives in the parent SKILL.md.
|
||||
|
||||
## Persistence scope
|
||||
|
||||
Per-user, cross-tenant. A user who completed `attack-paths@v1` in tenant
|
||||
A does not see the tour again in tenant B, even if they can access the
|
||||
feature there. The future `UserTourState` model (documented in
|
||||
`design.md`, not built) is FK to `User`, not `Membership`.
|
||||
|
||||
## Drift = #1 risk
|
||||
|
||||
Without the maintenance skill + the optional CI gate
|
||||
(`ui/scripts/check-tour-alignment.mjs`), tours decay silently as the
|
||||
covered UI evolves. The parent SKILL.md enumerates the six drift
|
||||
categories the skill checks for.
|
||||
@@ -411,64 +411,3 @@ class Test_check_security_group:
|
||||
port, port, TRANSPORT_PROTOCOL_ALL, [], [IP_V6_ALL_CIDRS]
|
||||
)
|
||||
assert check_security_group(ingress_rule, TRANSPORT_PROTOCOL_ALL, None, True)
|
||||
|
||||
# TCP Protocol - two halves of IPv4 in one rule - Ingress 22 to 22 - check 22 - Any Address - Open
|
||||
def test_split_ipv4_halves_open_22_tcp_any_address(self):
|
||||
port = 22
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
port, port, TRANSPORT_PROTOCOL_TCP, ["0.0.0.0/1", "128.0.0.0/1"], []
|
||||
)
|
||||
assert check_security_group(ingress_rule, TRANSPORT_PROTOCOL_TCP, [port], True)
|
||||
|
||||
# TCP Protocol - two halves of IPv6 in one rule - Ingress 22 to 22 - check 22 - Any Address - Open
|
||||
def test_split_ipv6_halves_open_22_tcp_any_address(self):
|
||||
port = 22
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
port, port, TRANSPORT_PROTOCOL_TCP, [], ["::/1", "8000::/1"]
|
||||
)
|
||||
assert check_security_group(ingress_rule, TRANSPORT_PROTOCOL_TCP, [port], True)
|
||||
|
||||
# Any protocol - two halves of IPv4 in one rule - check 22 - Any Address - Open
|
||||
def test_split_ipv4_halves_any_protocol_any_address(self):
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
0, 65535, TRANSPORT_PROTOCOL_ALL, ["0.0.0.0/1", "128.0.0.0/1"], []
|
||||
)
|
||||
assert check_security_group(ingress_rule, TRANSPORT_PROTOCOL_TCP, [22], True)
|
||||
|
||||
# TCP Protocol - one half of IPv4 - Ingress 22 to 22 - check 22 - Any Address - Closed
|
||||
def test_one_ipv4_half_open_22_tcp_any_address(self):
|
||||
port = 22
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
port, port, TRANSPORT_PROTOCOL_TCP, ["0.0.0.0/1"], []
|
||||
)
|
||||
assert not check_security_group(
|
||||
ingress_rule, TRANSPORT_PROTOCOL_TCP, [port], True
|
||||
)
|
||||
|
||||
# TCP Protocol - one half of IPv4 and one half of IPv6 - Ingress 22 to 22 - check 22 - Any Address - Closed
|
||||
def test_one_ipv4_half_and_one_ipv6_half_open_22_tcp_any_address(self):
|
||||
port = 22
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
port, port, TRANSPORT_PROTOCOL_TCP, ["0.0.0.0/1"], ["8000::/1"]
|
||||
)
|
||||
assert not check_security_group(
|
||||
ingress_rule, TRANSPORT_PROTOCOL_TCP, [port], True
|
||||
)
|
||||
|
||||
# TCP Protocol - two halves of IPv4 - Ingress 80 to 80 - check 22 - Any Address - Closed
|
||||
def test_split_ipv4_halves_other_port_any_address(self):
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
80, 80, TRANSPORT_PROTOCOL_TCP, ["0.0.0.0/1", "128.0.0.0/1"], []
|
||||
)
|
||||
assert not check_security_group(
|
||||
ingress_rule, TRANSPORT_PROTOCOL_TCP, [22], True
|
||||
)
|
||||
|
||||
# The rule passed in is not modified, since fixers send it back to AWS
|
||||
def test_split_ipv4_halves_rule_unchanged(self):
|
||||
ingress_rule = self.ingress_rule_generator(
|
||||
22, 22, TRANSPORT_PROTOCOL_TCP, ["0.0.0.0/1", "128.0.0.0/1"], []
|
||||
)
|
||||
before = repr(ingress_rule)
|
||||
check_security_group(ingress_rule, TRANSPORT_PROTOCOL_TCP, [22], True)
|
||||
assert repr(ingress_rule) == before
|
||||
+4
-7
@@ -13,11 +13,12 @@ from tests.providers.aws.utils import (
|
||||
|
||||
|
||||
class Test_emr_cluster_publicly_accesible:
|
||||
@mock_aws
|
||||
def test_no_clusters(self):
|
||||
# EMR Client
|
||||
emr_client = mock.MagicMock
|
||||
emr_client.clusters = {}
|
||||
# EC2 Client
|
||||
ec2_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -25,12 +26,8 @@ class Test_emr_cluster_publicly_accesible:
|
||||
new=emr_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.emr.emr_cluster_publicly_accesible.emr_cluster_publicly_accesible.ec2_client",
|
||||
new=mock.MagicMock(),
|
||||
"prowler.providers.aws.services.ec2.ec2_service.EC2",
|
||||
new=ec2_client,
|
||||
),
|
||||
):
|
||||
# Test Check
|
||||
|
||||
@@ -172,64 +172,3 @@ class Test_IAM_get_role_assignments:
|
||||
filter="atScope()"
|
||||
)
|
||||
assert AZURE_SUBSCRIPTION_ID in result
|
||||
|
||||
def test_get_role_assignments_marks_incomplete_read(self):
|
||||
owner_assignment = MagicMock(
|
||||
id="assignment-1",
|
||||
scope=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}",
|
||||
principal_id="principal-1",
|
||||
principal_type="User",
|
||||
role_definition_id="/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
|
||||
)
|
||||
owner_assignment.name = "assignment-1"
|
||||
|
||||
def pages():
|
||||
yield owner_assignment
|
||||
raise Exception("next page failed")
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.role_assignments.list_for_subscription.return_value = pages()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.azure.services.iam.iam_service.IAM._get_roles",
|
||||
return_value=({}, {}),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.azure.services.iam.iam_service.IAM._get_role_assignments",
|
||||
return_value={},
|
||||
),
|
||||
):
|
||||
iam = IAM(set_mocked_azure_provider())
|
||||
|
||||
iam.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
|
||||
iam.resource_groups = None
|
||||
|
||||
result = iam._get_role_assignments()
|
||||
|
||||
assert list(result[AZURE_SUBSCRIPTION_ID]) == ["assignment-1"]
|
||||
assert iam.role_assignments_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_get_role_assignments_complete_read_is_not_marked(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.role_assignments.list_for_subscription.return_value = []
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.azure.services.iam.iam_service.IAM._get_roles",
|
||||
return_value=({}, {}),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.azure.services.iam.iam_service.IAM._get_role_assignments",
|
||||
return_value={},
|
||||
),
|
||||
):
|
||||
iam = IAM(set_mocked_azure_provider())
|
||||
|
||||
iam.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
|
||||
iam.resource_groups = None
|
||||
|
||||
result = iam._get_role_assignments()
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert iam.role_assignments_lookup_failed == set()
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.azure.config import OWNER_ROLE_ID
|
||||
from prowler.providers.azure.services.iam.iam_service import RoleAssignment
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_DISPLAY,
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
AZURE_SUBSCRIPTION_NAME,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
CHECK = "iam_subscription_owners_between_2_and_3"
|
||||
CHECK_PATH = f"prowler.providers.azure.services.iam.{CHECK}.{CHECK}"
|
||||
SUBSCRIPTION_SCOPE = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}"
|
||||
MANAGEMENT_GROUP_SCOPE = "/providers/Microsoft.Management/managementGroups/mg1"
|
||||
READER_ROLE_ID = "acdd72a7-3385-48ef-bd42-f606fba81ae7"
|
||||
|
||||
|
||||
def build_assignment(
|
||||
role_id: str, scope: str = SUBSCRIPTION_SCOPE, agent_id: str = None
|
||||
) -> RoleAssignment:
|
||||
assignment_name = str(uuid4())
|
||||
return RoleAssignment(
|
||||
id=f"{scope}/providers/Microsoft.Authorization/roleAssignments/{assignment_name}",
|
||||
name=assignment_name,
|
||||
scope=scope,
|
||||
agent_id=agent_id or str(uuid4()),
|
||||
agent_type="User",
|
||||
role_id=role_id,
|
||||
)
|
||||
|
||||
|
||||
def run_check(role_assignments, lookup_failed=()):
|
||||
iam_client = mock.MagicMock()
|
||||
iam_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
iam_client.role_assignments = role_assignments
|
||||
iam_client.role_assignments_lookup_failed = set(lookup_failed)
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(f"{CHECK_PATH}.iam_client", new=iam_client),
|
||||
):
|
||||
from prowler.providers.azure.services.iam.iam_subscription_owners_between_2_and_3.iam_subscription_owners_between_2_and_3 import (
|
||||
iam_subscription_owners_between_2_and_3,
|
||||
)
|
||||
|
||||
return iam_subscription_owners_between_2_and_3().execute()
|
||||
|
||||
|
||||
def run_with(*assignments):
|
||||
by_id = {assignment.id: assignment for assignment in assignments}
|
||||
return run_check({AZURE_SUBSCRIPTION_ID: by_id})
|
||||
|
||||
|
||||
class Test_iam_subscription_owners_between_2_and_3:
|
||||
def test_no_subscriptions(self):
|
||||
assert run_check({}) == []
|
||||
|
||||
def test_role_assignments_lookup_failed_is_manual(self):
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: {}}, lookup_failed={AZURE_SUBSCRIPTION_ID}
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Cannot evaluate the owner count of subscription {AZURE_SUBSCRIPTION_DISPLAY}: its role assignments could not be read completely. Verify that Microsoft.Authorization/roleAssignments/read (included in Reader) is granted to the scanning identity."
|
||||
)
|
||||
assert result[0].resource_id == SUBSCRIPTION_SCOPE
|
||||
|
||||
def test_partial_read_with_two_owners_is_manual(self):
|
||||
owners = [build_assignment(OWNER_ROLE_ID) for _ in range(2)]
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: {owner.id: owner for owner in owners}},
|
||||
lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_no_role_assignments_fail(self):
|
||||
result = run_check({AZURE_SUBSCRIPTION_ID: {}})
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Subscription {AZURE_SUBSCRIPTION_DISPLAY} has an owner count of 0 (0 direct, 0 inherited), outside the recommended 2 to 3."
|
||||
)
|
||||
|
||||
def test_two_direct_owners_pass(self):
|
||||
result = run_with(
|
||||
build_assignment(OWNER_ROLE_ID),
|
||||
build_assignment(OWNER_ROLE_ID),
|
||||
build_assignment(READER_ROLE_ID),
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Subscription {AZURE_SUBSCRIPTION_DISPLAY} has an owner count of 2 (2 direct, 0 inherited), within the recommended 2 to 3."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_id == SUBSCRIPTION_SCOPE
|
||||
assert result[0].resource_name == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_three_owners_pass(self):
|
||||
result = run_with(*[build_assignment(OWNER_ROLE_ID) for _ in range(3)])
|
||||
assert result[0].status == "PASS"
|
||||
assert "owner count of 3 (3 direct, 0 inherited)" in result[0].status_extended
|
||||
|
||||
def test_one_owner_fail(self):
|
||||
result = run_with(
|
||||
build_assignment(OWNER_ROLE_ID), build_assignment(READER_ROLE_ID)
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Subscription {AZURE_SUBSCRIPTION_DISPLAY} has an owner count of 1 (1 direct, 0 inherited), outside the recommended 2 to 3."
|
||||
)
|
||||
|
||||
def test_four_owners_fail(self):
|
||||
result = run_with(*[build_assignment(OWNER_ROLE_ID) for _ in range(4)])
|
||||
assert result[0].status == "FAIL"
|
||||
assert "owner count of 4" in result[0].status_extended
|
||||
|
||||
def test_inherited_owner_counted(self):
|
||||
result = run_with(
|
||||
build_assignment(OWNER_ROLE_ID),
|
||||
build_assignment(OWNER_ROLE_ID, scope=MANAGEMENT_GROUP_SCOPE),
|
||||
)
|
||||
assert result[0].status == "PASS"
|
||||
assert "owner count of 2 (1 direct, 1 inherited)" in result[0].status_extended
|
||||
|
||||
def test_owner_at_both_scopes_counted_once(self):
|
||||
agent_id = str(uuid4())
|
||||
result = run_with(
|
||||
build_assignment(OWNER_ROLE_ID, agent_id=agent_id),
|
||||
build_assignment(
|
||||
OWNER_ROLE_ID, scope=MANAGEMENT_GROUP_SCOPE, agent_id=agent_id
|
||||
),
|
||||
)
|
||||
assert result[0].status == "FAIL"
|
||||
assert "owner count of 1 (1 direct, 0 inherited)" in result[0].status_extended
|
||||
|
||||
def test_owner_at_both_scopes_with_different_casing_counted_once(self):
|
||||
agent_id = str(uuid4())
|
||||
result = run_with(
|
||||
build_assignment(OWNER_ROLE_ID, agent_id=agent_id),
|
||||
build_assignment(
|
||||
OWNER_ROLE_ID, scope=MANAGEMENT_GROUP_SCOPE, agent_id=agent_id.upper()
|
||||
),
|
||||
)
|
||||
assert result[0].status == "FAIL"
|
||||
assert "owner count of 1 (1 direct, 0 inherited)" in result[0].status_extended
|
||||
|
||||
def test_owner_role_id_matched_case_insensitively(self):
|
||||
result = run_with(
|
||||
build_assignment(OWNER_ROLE_ID.upper()),
|
||||
build_assignment(OWNER_ROLE_ID.upper()),
|
||||
)
|
||||
assert result[0].status == "PASS"
|
||||
assert "owner count of 2 (2 direct, 0 inherited)" in result[0].status_extended
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.azure.services.network.network_service import (
|
||||
ApplicationGateway,
|
||||
)
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_DISPLAY,
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
AZURE_SUBSCRIPTION_NAME,
|
||||
RESOURCE_GROUP,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
CHECK = "network_application_gateway_http2_enabled"
|
||||
CHECK_PATH = f"prowler.providers.azure.services.network.{CHECK}.{CHECK}"
|
||||
GATEWAY_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.Network/applicationGateways/agw1"
|
||||
PREFIX = f"Application Gateway agw1 in subscription {AZURE_SUBSCRIPTION_DISPLAY}"
|
||||
|
||||
|
||||
def run_check(gateways, lookup_failed=()):
|
||||
network_client = mock.MagicMock()
|
||||
network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
network_client.application_gateways = gateways
|
||||
network_client.application_gateways_lookup_failed = set(lookup_failed)
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(f"{CHECK_PATH}.network_client", new=network_client),
|
||||
):
|
||||
from prowler.providers.azure.services.network.network_application_gateway_http2_enabled.network_application_gateway_http2_enabled import (
|
||||
network_application_gateway_http2_enabled,
|
||||
)
|
||||
|
||||
return network_application_gateway_http2_enabled().execute()
|
||||
|
||||
|
||||
def single(http2_enabled: bool):
|
||||
gateway = ApplicationGateway(
|
||||
id=GATEWAY_ID, name="agw1", location="westeurope", http2_enabled=http2_enabled
|
||||
)
|
||||
return run_check({AZURE_SUBSCRIPTION_ID: {GATEWAY_ID: gateway}})
|
||||
|
||||
|
||||
UNLISTED = f"Cannot evaluate HTTP/2 on the Application Gateways in subscription {AZURE_SUBSCRIPTION_DISPLAY}: they could not be listed completely. Check the scan log for the error, and verify that Microsoft.Network/applicationGateways/read (included in Reader) is granted to the scanning identity."
|
||||
|
||||
|
||||
def assert_gateway_finding(finding, status, status_extended):
|
||||
assert finding.status == status
|
||||
assert finding.status_extended == status_extended
|
||||
assert finding.subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert finding.resource_id == GATEWAY_ID
|
||||
assert finding.resource_name == "agw1"
|
||||
assert finding.location == "westeurope"
|
||||
|
||||
|
||||
def assert_subscription_finding(finding):
|
||||
assert finding.status == "MANUAL"
|
||||
assert finding.status_extended == UNLISTED
|
||||
assert finding.subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert finding.resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}"
|
||||
assert finding.resource_name == AZURE_SUBSCRIPTION_ID
|
||||
assert finding.location == "global"
|
||||
|
||||
|
||||
class Test_network_application_gateway_http2_enabled:
|
||||
def test_no_gateways(self):
|
||||
assert run_check({}) == []
|
||||
assert run_check({AZURE_SUBSCRIPTION_ID: {}}) == []
|
||||
|
||||
def test_http2_enabled(self):
|
||||
result = single(True)
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(result[0], "PASS", f"{PREFIX} has HTTP/2 enabled.")
|
||||
|
||||
def test_http2_disabled(self):
|
||||
result = single(False)
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0], "FAIL", f"{PREFIX} does not have HTTP/2 enabled."
|
||||
)
|
||||
|
||||
def test_gateways_lookup_failed_is_manual(self):
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: {}}, lookup_failed={AZURE_SUBSCRIPTION_ID}
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_subscription_finding(result[0])
|
||||
|
||||
def test_partial_lookup_still_reports_listed_gateways(self):
|
||||
gateway = ApplicationGateway(
|
||||
id=GATEWAY_ID, name="agw1", location="westeurope", http2_enabled=True
|
||||
)
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: {GATEWAY_ID: gateway}},
|
||||
lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert_subscription_finding(result[0])
|
||||
assert_gateway_finding(result[1], "PASS", f"{PREFIX} has HTTP/2 enabled.")
|
||||
-260
@@ -1,260 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.azure.services.network.network_service import (
|
||||
ApplicationGateway,
|
||||
)
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_DISPLAY,
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
AZURE_SUBSCRIPTION_NAME,
|
||||
RESOURCE_GROUP,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
CHECK = "network_application_gateway_waf_enabled"
|
||||
CHECK_PATH = f"prowler.providers.azure.services.network.{CHECK}.{CHECK}"
|
||||
GATEWAY_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.Network/applicationGateways/agw1"
|
||||
POLICY_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/waf1"
|
||||
PREFIX = f"Application Gateway agw1 in subscription {AZURE_SUBSCRIPTION_DISPLAY}"
|
||||
GATEWAYS_UNLISTED = f"Cannot evaluate the WAF policy association of the Application Gateways in subscription {AZURE_SUBSCRIPTION_DISPLAY}: they could not be listed completely. Check the scan log for the error, and verify that Microsoft.Network/applicationGateways/read (included in Reader) is granted to the scanning identity."
|
||||
POLICIES_UNLISTED = f"Cannot evaluate the WAF policy association of the Application Gateways in subscription {AZURE_SUBSCRIPTION_DISPLAY}: the WAF policies associated with them could not be listed or read completely. Check the scan log for the error, and verify that Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/read (included in Reader) is granted to the scanning identity."
|
||||
BOTH_UNLISTED = f"Cannot evaluate the WAF policy association of the Application Gateways in subscription {AZURE_SUBSCRIPTION_DISPLAY}: they could not be listed completely, and the WAF policies associated with them could not be listed or read completely. Check the scan log for the errors, and verify that Microsoft.Network/applicationGateways/read and Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/read (both included in Reader) are granted to the scanning identity."
|
||||
|
||||
|
||||
def build_gateway(**settings) -> ApplicationGateway:
|
||||
return ApplicationGateway(
|
||||
id=GATEWAY_ID, name="agw1", location="westeurope", **settings
|
||||
)
|
||||
|
||||
|
||||
def run_check(
|
||||
gateways,
|
||||
gateways_lookup_failed=(),
|
||||
policies_lookup_failed=(),
|
||||
policy_reads_failed=(),
|
||||
):
|
||||
network_client = mock.MagicMock()
|
||||
network_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
network_client.application_gateways = gateways
|
||||
network_client.application_gateways_lookup_failed = set(gateways_lookup_failed)
|
||||
network_client.waf_policies_lookup_failed = set(policies_lookup_failed)
|
||||
network_client.waf_policy_reads_failed = set(policy_reads_failed)
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(f"{CHECK_PATH}.network_client", new=network_client),
|
||||
):
|
||||
from prowler.providers.azure.services.network.network_application_gateway_waf_enabled.network_application_gateway_waf_enabled import (
|
||||
network_application_gateway_waf_enabled,
|
||||
)
|
||||
|
||||
return network_application_gateway_waf_enabled().execute()
|
||||
|
||||
|
||||
def single(gateway):
|
||||
return run_check({AZURE_SUBSCRIPTION_ID: {gateway.id: gateway}})
|
||||
|
||||
|
||||
def assert_gateway_finding(finding, status, status_extended):
|
||||
assert finding.status == status
|
||||
assert finding.status_extended == status_extended
|
||||
assert finding.subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert finding.resource_id == GATEWAY_ID
|
||||
assert finding.resource_name == "agw1"
|
||||
assert finding.location == "westeurope"
|
||||
|
||||
|
||||
def assert_subscription_finding(finding, status_extended):
|
||||
assert finding.status == "MANUAL"
|
||||
assert finding.status_extended == status_extended
|
||||
assert finding.subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert finding.resource_id == f"/subscriptions/{AZURE_SUBSCRIPTION_ID}"
|
||||
assert finding.resource_name == AZURE_SUBSCRIPTION_ID
|
||||
assert finding.location == "global"
|
||||
|
||||
|
||||
class Test_network_application_gateway_waf_enabled:
|
||||
def test_no_gateways(self):
|
||||
assert run_check({}) == []
|
||||
assert run_check({AZURE_SUBSCRIPTION_ID: {}}) == []
|
||||
|
||||
def test_enabled_waf_policy_associated(self):
|
||||
result = single(
|
||||
build_gateway(firewall_policy_id=POLICY_ID, firewall_policy_enabled=True)
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0], "PASS", f"{PREFIX} has an enabled WAF policy associated."
|
||||
)
|
||||
|
||||
def test_disabled_waf_policy_associated(self):
|
||||
result = single(
|
||||
build_gateway(firewall_policy_id=POLICY_ID, firewall_policy_enabled=False)
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0],
|
||||
"FAIL",
|
||||
f"{PREFIX} has a WAF policy associated, but the policy is disabled.",
|
||||
)
|
||||
|
||||
def test_waf_policy_state_unknown(self):
|
||||
result = single(build_gateway(firewall_policy_id=POLICY_ID))
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0],
|
||||
"MANUAL",
|
||||
f"{PREFIX} has a WAF policy associated, but the state of that policy could not be determined. Check the scan log for the error.",
|
||||
)
|
||||
|
||||
def test_legacy_waf_configuration(self):
|
||||
result = single(build_gateway(waf_configuration_enabled=True))
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0],
|
||||
"FAIL",
|
||||
f"{PREFIX} uses the legacy WAF configuration instead of a WAF policy.",
|
||||
)
|
||||
|
||||
def test_no_waf(self):
|
||||
result = single(build_gateway())
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0], "FAIL", f"{PREFIX} has no gateway-level WAF policy associated."
|
||||
)
|
||||
|
||||
def test_gateways_lookup_failed_is_manual(self):
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: {}}, gateways_lookup_failed={AZURE_SUBSCRIPTION_ID}
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_subscription_finding(result[0], GATEWAYS_UNLISTED)
|
||||
|
||||
def test_partial_gateways_lookup_still_reports_listed_gateways(self):
|
||||
gateway = build_gateway(
|
||||
firewall_policy_id=POLICY_ID, firewall_policy_enabled=True
|
||||
)
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: {GATEWAY_ID: gateway}},
|
||||
gateways_lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert_subscription_finding(result[0], GATEWAYS_UNLISTED)
|
||||
assert_gateway_finding(
|
||||
result[1], "PASS", f"{PREFIX} has an enabled WAF policy associated."
|
||||
)
|
||||
|
||||
def test_policies_lookup_failed_reports_unknown_states_once(self):
|
||||
other_id = GATEWAY_ID.replace("agw1", "agw2")
|
||||
gateways = {
|
||||
GATEWAY_ID: build_gateway(firewall_policy_id=POLICY_ID),
|
||||
other_id: ApplicationGateway(
|
||||
id=other_id,
|
||||
name="agw2",
|
||||
location="westeurope",
|
||||
firewall_policy_id=POLICY_ID,
|
||||
),
|
||||
}
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: gateways},
|
||||
policies_lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_subscription_finding(result[0], POLICIES_UNLISTED)
|
||||
|
||||
def test_policies_lookup_failed_keeps_gateways_with_known_states(self):
|
||||
other_id = GATEWAY_ID.replace("agw1", "agw2")
|
||||
gateways = {
|
||||
GATEWAY_ID: build_gateway(
|
||||
firewall_policy_id=POLICY_ID, firewall_policy_enabled=False
|
||||
),
|
||||
other_id: ApplicationGateway(
|
||||
id=other_id,
|
||||
name="agw2",
|
||||
location="westeurope",
|
||||
firewall_policy_id=POLICY_ID.replace("waf1", "waf2"),
|
||||
),
|
||||
}
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: gateways},
|
||||
policies_lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert_subscription_finding(result[0], POLICIES_UNLISTED)
|
||||
assert_gateway_finding(
|
||||
result[1],
|
||||
"FAIL",
|
||||
f"{PREFIX} has a WAF policy associated, but the policy is disabled.",
|
||||
)
|
||||
|
||||
def test_policies_lookup_failed_without_unknown_states_adds_nothing(self):
|
||||
result = run_check(
|
||||
{
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
GATEWAY_ID: build_gateway(
|
||||
firewall_policy_id=POLICY_ID, firewall_policy_enabled=True
|
||||
)
|
||||
}
|
||||
},
|
||||
policies_lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_gateway_finding(
|
||||
result[0], "PASS", f"{PREFIX} has an enabled WAF policy associated."
|
||||
)
|
||||
|
||||
def test_failed_policy_read_reports_unknown_states_once(self):
|
||||
other_id = GATEWAY_ID.replace("agw1", "agw2")
|
||||
gateways = {
|
||||
GATEWAY_ID: build_gateway(firewall_policy_id=POLICY_ID),
|
||||
other_id: ApplicationGateway(
|
||||
id=other_id,
|
||||
name="agw2",
|
||||
location="westeurope",
|
||||
firewall_policy_id=POLICY_ID,
|
||||
),
|
||||
}
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: gateways},
|
||||
policy_reads_failed={POLICY_ID.casefold()},
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_subscription_finding(result[0], POLICIES_UNLISTED)
|
||||
|
||||
def test_failed_policy_read_keeps_gateway_with_missing_policy(self):
|
||||
other_id = GATEWAY_ID.replace("agw1", "agw2")
|
||||
gateways = {
|
||||
GATEWAY_ID: build_gateway(firewall_policy_id=POLICY_ID),
|
||||
other_id: ApplicationGateway(
|
||||
id=other_id,
|
||||
name="agw2",
|
||||
location="westeurope",
|
||||
firewall_policy_id=POLICY_ID.replace("waf1", "waf2"),
|
||||
),
|
||||
}
|
||||
result = run_check(
|
||||
{AZURE_SUBSCRIPTION_ID: gateways},
|
||||
policy_reads_failed={POLICY_ID.upper().casefold()},
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert_subscription_finding(result[0], POLICIES_UNLISTED)
|
||||
assert result[1].resource_id == other_id
|
||||
assert result[1].status == "MANUAL"
|
||||
assert result[1].status_extended == (
|
||||
f"Application Gateway agw2 in subscription {AZURE_SUBSCRIPTION_DISPLAY} has a WAF policy associated, but the state of that policy could not be determined. Check the scan log for the error."
|
||||
)
|
||||
|
||||
def test_unlisted_gateways_and_policies_give_one_finding(self):
|
||||
result = run_check(
|
||||
{
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
GATEWAY_ID: build_gateway(firewall_policy_id=POLICY_ID)
|
||||
}
|
||||
},
|
||||
gateways_lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
policies_lookup_failed={AZURE_SUBSCRIPTION_ID},
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert_subscription_finding(result[0], BOTH_UNLISTED)
|
||||
@@ -1,23 +1,14 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from azure.mgmt.network import models as azure_network_models
|
||||
from azure.mgmt.network.models import (
|
||||
ApplicationGatewaySku,
|
||||
ApplicationGatewayWebApplicationFirewallConfiguration,
|
||||
FlowLog,
|
||||
SubResource,
|
||||
)
|
||||
from azure.mgmt.network.models import FlowLog
|
||||
|
||||
from prowler.providers.azure.services.network.network_service import (
|
||||
ApplicationGateway,
|
||||
BastionHost,
|
||||
Network,
|
||||
NetworkWatcher,
|
||||
PublicIp,
|
||||
SecurityGroup,
|
||||
VirtualNetwork,
|
||||
WafPolicy,
|
||||
)
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
@@ -92,30 +83,6 @@ def mock_network_get_virtual_networks(_):
|
||||
}
|
||||
|
||||
|
||||
def mock_network_get_waf_policies(_):
|
||||
return {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
"waf_id": WafPolicy(
|
||||
id="waf_id", name="waf_name", location="location", state="Enabled"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def mock_network_get_application_gateways(_):
|
||||
return {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
"id": ApplicationGateway(
|
||||
id="id",
|
||||
name="name",
|
||||
location="location",
|
||||
firewall_policy_id="policy_id",
|
||||
http2_enabled=True,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_security_groups",
|
||||
new=mock_network_get_security_groups,
|
||||
@@ -136,14 +103,6 @@ def mock_network_get_application_gateways(_):
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_virtual_networks",
|
||||
new=mock_network_get_virtual_networks,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_Service:
|
||||
def test_get_client(self):
|
||||
network = Network(set_mocked_azure_provider())
|
||||
@@ -163,21 +122,6 @@ class Test_Network_Service:
|
||||
assert network.security_groups[AZURE_SUBSCRIPTION_ID][0].location == "location"
|
||||
assert network.security_groups[AZURE_SUBSCRIPTION_ID][0].security_rules == []
|
||||
|
||||
def test_get_waf_policies(self):
|
||||
network = Network(set_mocked_azure_provider())
|
||||
policy = network.waf_policies[AZURE_SUBSCRIPTION_ID]["waf_id"]
|
||||
assert policy.__class__.__name__ == "WafPolicy"
|
||||
assert policy.name == "waf_name"
|
||||
assert policy.state == "Enabled"
|
||||
|
||||
def test_get_application_gateways(self):
|
||||
network = Network(set_mocked_azure_provider())
|
||||
gateway = network.application_gateways[AZURE_SUBSCRIPTION_ID]["id"]
|
||||
assert gateway.__class__.__name__ == "ApplicationGateway"
|
||||
assert gateway.name == "name"
|
||||
assert gateway.firewall_policy_id == "policy_id"
|
||||
assert gateway.http2_enabled is True
|
||||
|
||||
def test_get_network_watchers(self):
|
||||
network = Network(set_mocked_azure_provider())
|
||||
assert (
|
||||
@@ -241,14 +185,6 @@ class Test_Network_Service:
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_security_groups:
|
||||
def test_get_security_groups_no_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -418,14 +354,6 @@ class Test_Network_get_security_groups:
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_network_watchers:
|
||||
def test_get_network_watchers_no_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -530,14 +458,6 @@ class Test_Network_get_network_watchers:
|
||||
assert AZURE_SUBSCRIPTION_ID in result
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_bastion_hosts:
|
||||
def test_get_bastion_hosts_no_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -643,14 +563,6 @@ class Test_Network_get_bastion_hosts:
|
||||
assert result[AZURE_SUBSCRIPTION_ID] == []
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_public_ip_addresses:
|
||||
def test_get_public_ip_addresses_no_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -756,14 +668,6 @@ class Test_Network_get_public_ip_addresses:
|
||||
assert result[AZURE_SUBSCRIPTION_ID] == []
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_network_watchers_extra:
|
||||
def test_get_network_watchers_with_multiple_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -833,14 +737,6 @@ class Test_Network_get_network_watchers_extra:
|
||||
mock_client.network_watchers.list.assert_not_called()
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_bastion_hosts_extra:
|
||||
def test_get_bastion_hosts_with_multiple_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -910,14 +806,6 @@ class Test_Network_get_bastion_hosts_extra:
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_public_ip_addresses_extra:
|
||||
def test_get_public_ip_addresses_with_multiple_resource_groups(self):
|
||||
mock_client = MagicMock()
|
||||
@@ -987,14 +875,6 @@ class Test_Network_get_public_ip_addresses_extra:
|
||||
)
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_waf_policies",
|
||||
new=mock_network_get_waf_policies,
|
||||
)
|
||||
@patch(
|
||||
"prowler.providers.azure.services.network.network_service.Network._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
)
|
||||
class Test_Network_get_virtual_networks_extra:
|
||||
def _ctx(self):
|
||||
return (
|
||||
@@ -1205,395 +1085,3 @@ class Test_Network_get_virtual_networks_extra:
|
||||
mock_client.virtual_networks.list.assert_called_once_with(
|
||||
resource_group_name="RG"
|
||||
)
|
||||
|
||||
|
||||
NETWORK = "prowler.providers.azure.services.network.network_service.Network"
|
||||
GATEWAY_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.Network/applicationGateways/agw1"
|
||||
GATEWAY_POLICY_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/waf1"
|
||||
|
||||
|
||||
def build_network_with_client(mock_client, resource_groups=None):
|
||||
with (
|
||||
patch(f"{NETWORK}._get_security_groups", new=mock_network_get_security_groups),
|
||||
patch(f"{NETWORK}._get_bastion_hosts", new=mock_network_get_bastion_hosts),
|
||||
patch(
|
||||
f"{NETWORK}._get_network_watchers", new=mock_network_get_network_watchers
|
||||
),
|
||||
patch(
|
||||
f"{NETWORK}._get_public_ip_addresses",
|
||||
new=mock_network_get_public_ip_addresses,
|
||||
),
|
||||
patch(
|
||||
f"{NETWORK}._get_virtual_networks", new=mock_network_get_virtual_networks
|
||||
),
|
||||
patch(f"{NETWORK}._get_waf_policies", new=mock_network_get_waf_policies),
|
||||
patch(
|
||||
f"{NETWORK}._get_application_gateways",
|
||||
new=mock_network_get_application_gateways,
|
||||
),
|
||||
):
|
||||
network = Network(set_mocked_azure_provider())
|
||||
network.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
|
||||
network.resource_groups = resource_groups
|
||||
return network
|
||||
|
||||
|
||||
def build_sdk_waf_policy(state, policy_id=GATEWAY_POLICY_ID, name="waf1"):
|
||||
waf_policy = azure_network_models.WebApplicationFirewallPolicy(
|
||||
id=policy_id,
|
||||
location="westeurope",
|
||||
policy_settings=azure_network_models.PolicySettings(state=state),
|
||||
)
|
||||
waf_policy.name = name
|
||||
return waf_policy
|
||||
|
||||
|
||||
def build_waf_policy_model(state, policy_id=GATEWAY_POLICY_ID):
|
||||
return WafPolicy(id=policy_id, name="waf1", location="westeurope", state=state)
|
||||
|
||||
|
||||
def build_sdk_application_gateway(name="agw1", policy_id=None):
|
||||
gateway = azure_network_models.ApplicationGateway(
|
||||
id=GATEWAY_ID.replace("agw1", name),
|
||||
location="westeurope",
|
||||
sku=ApplicationGatewaySku(name="WAF_v2", tier="WAF_v2"),
|
||||
firewall_policy=SubResource(id=policy_id) if policy_id else None,
|
||||
)
|
||||
gateway.name = name
|
||||
return gateway
|
||||
|
||||
|
||||
def get_gateways(mock_client, listed_policies=(), resource_groups=None):
|
||||
"""Collect gateways with the given WAF policies already listed."""
|
||||
network = build_network_with_client(mock_client, resource_groups)
|
||||
network.waf_policies = {
|
||||
AZURE_SUBSCRIPTION_ID: {policy.id: policy for policy in listed_policies}
|
||||
}
|
||||
return network, network._get_application_gateways()
|
||||
|
||||
|
||||
class Test_Network_get_waf_policies:
|
||||
def test_maps_policy_state(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.web_application_firewall_policies.list_all.return_value = [
|
||||
build_sdk_waf_policy("Enabled")
|
||||
]
|
||||
|
||||
network = build_network_with_client(mock_client)
|
||||
result = network._get_waf_policies()
|
||||
|
||||
assert network.waf_policies_lookup_failed == set()
|
||||
policy = result[AZURE_SUBSCRIPTION_ID][GATEWAY_POLICY_ID]
|
||||
assert policy.id == GATEWAY_POLICY_ID
|
||||
assert policy.name == "waf1"
|
||||
assert policy.location == "westeurope"
|
||||
assert policy.state == "Enabled"
|
||||
|
||||
def test_uses_resource_group_scope(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.web_application_firewall_policies.list.return_value = []
|
||||
|
||||
network = build_network_with_client(
|
||||
mock_client, {AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
|
||||
)
|
||||
result = network._get_waf_policies()
|
||||
|
||||
mock_client.web_application_firewall_policies.list.assert_called_once_with(
|
||||
resource_group_name=RESOURCE_GROUP
|
||||
)
|
||||
mock_client.web_application_firewall_policies.list_all.assert_not_called()
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
|
||||
def test_api_error_marks_subscription_lookup_failed(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.web_application_firewall_policies.list_all.side_effect = Exception(
|
||||
"boom"
|
||||
)
|
||||
|
||||
network = build_network_with_client(mock_client)
|
||||
result = network._get_waf_policies()
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert network.waf_policies_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_resource_group_error_marks_subscription_lookup_failed(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.web_application_firewall_policies.list.side_effect = Exception(
|
||||
"boom"
|
||||
)
|
||||
|
||||
network = build_network_with_client(
|
||||
mock_client, {AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
|
||||
)
|
||||
result = network._get_waf_policies()
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert network.waf_policies_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_missing_resource_group_is_not_a_failed_lookup(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.web_application_firewall_policies.list.side_effect = (
|
||||
ResourceNotFoundError("gone")
|
||||
)
|
||||
|
||||
network = build_network_with_client(
|
||||
mock_client, {AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
|
||||
)
|
||||
result = network._get_waf_policies()
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert network.waf_policies_lookup_failed == set()
|
||||
|
||||
def test_unmappable_policy_is_skipped_and_marks_subscription(self):
|
||||
broken = azure_network_models.WebApplicationFirewallPolicy(
|
||||
location="westeurope"
|
||||
)
|
||||
mock_client = MagicMock()
|
||||
mock_client.web_application_firewall_policies.list_all.return_value = [
|
||||
broken,
|
||||
build_sdk_waf_policy("Enabled"),
|
||||
]
|
||||
|
||||
network = build_network_with_client(mock_client)
|
||||
result = network._get_waf_policies()
|
||||
|
||||
assert list(result[AZURE_SUBSCRIPTION_ID]) == [GATEWAY_POLICY_ID]
|
||||
assert network.waf_policies_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
|
||||
class Test_Network_get_application_gateways:
|
||||
def test_maps_gateway_settings(self):
|
||||
gateway = azure_network_models.ApplicationGateway(
|
||||
id=GATEWAY_ID,
|
||||
location="westeurope",
|
||||
sku=ApplicationGatewaySku(name="WAF_v2", tier="WAF_v2"),
|
||||
firewall_policy=SubResource(id=GATEWAY_POLICY_ID),
|
||||
enable_http2=True,
|
||||
)
|
||||
gateway.name = "agw1"
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [gateway]
|
||||
|
||||
network, result = get_gateways(mock_client, [build_waf_policy_model("Enabled")])
|
||||
|
||||
assert network.application_gateways_lookup_failed == set()
|
||||
mock_client.web_application_firewall_policies.get.assert_not_called()
|
||||
mapped = result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID]
|
||||
assert mapped.name == "agw1"
|
||||
assert mapped.location == "westeurope"
|
||||
assert mapped.firewall_policy_id == GATEWAY_POLICY_ID
|
||||
assert mapped.firewall_policy_enabled is True
|
||||
assert mapped.waf_configuration_enabled is False
|
||||
assert mapped.http2_enabled is True
|
||||
|
||||
def test_maps_legacy_waf_configuration(self):
|
||||
waf_configuration = ApplicationGatewayWebApplicationFirewallConfiguration(
|
||||
enabled=True,
|
||||
firewall_mode="Prevention",
|
||||
rule_set_type="OWASP",
|
||||
rule_set_version="3.2",
|
||||
)
|
||||
gateway = azure_network_models.ApplicationGateway(
|
||||
id=GATEWAY_ID,
|
||||
location="westeurope",
|
||||
sku=ApplicationGatewaySku(name="WAF_v2", tier="WAF_v2"),
|
||||
web_application_firewall_configuration=waf_configuration,
|
||||
)
|
||||
gateway.name = "agw1"
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [gateway]
|
||||
|
||||
_, result = get_gateways(mock_client)
|
||||
|
||||
mapped = result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID]
|
||||
assert mapped.firewall_policy_id is None
|
||||
assert mapped.firewall_policy_enabled is None
|
||||
assert mapped.waf_configuration_enabled is True
|
||||
assert mapped.http2_enabled is False
|
||||
|
||||
def test_uses_resource_group_scope(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list.return_value = []
|
||||
|
||||
_, result = get_gateways(
|
||||
mock_client, resource_groups={AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
|
||||
)
|
||||
|
||||
mock_client.application_gateways.list.assert_called_once_with(
|
||||
resource_group_name=RESOURCE_GROUP
|
||||
)
|
||||
mock_client.application_gateways.list_all.assert_not_called()
|
||||
assert result[AZURE_SUBSCRIPTION_ID] == {}
|
||||
|
||||
def test_api_error_marks_subscription_lookup_failed(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.side_effect = Exception("boom")
|
||||
|
||||
network, result = get_gateways(mock_client)
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert network.application_gateways_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_resource_group_error_marks_subscription_lookup_failed(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list.side_effect = Exception("boom")
|
||||
|
||||
network, result = get_gateways(
|
||||
mock_client, resource_groups={AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
|
||||
)
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert network.application_gateways_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_maps_disabled_waf_policy_whatever_its_casing(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=GATEWAY_POLICY_ID.upper())
|
||||
]
|
||||
|
||||
_, result = get_gateways(mock_client, [build_waf_policy_model("disabled")])
|
||||
|
||||
assert (
|
||||
result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is False
|
||||
)
|
||||
|
||||
def test_unlisted_waf_policy_is_read_by_id(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=GATEWAY_POLICY_ID)
|
||||
]
|
||||
mock_client.web_application_firewall_policies.get.return_value = (
|
||||
build_sdk_waf_policy("Enabled")
|
||||
)
|
||||
|
||||
_, result = get_gateways(mock_client)
|
||||
|
||||
mock_client.web_application_firewall_policies.get.assert_called_once_with(
|
||||
resource_group_name=RESOURCE_GROUP, policy_name="waf1"
|
||||
)
|
||||
assert result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is True
|
||||
|
||||
def test_waf_policy_read_error_is_recorded_and_leaves_state_unknown(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=GATEWAY_POLICY_ID)
|
||||
]
|
||||
mock_client.web_application_firewall_policies.get.side_effect = Exception(
|
||||
"boom"
|
||||
)
|
||||
|
||||
network, result = get_gateways(mock_client)
|
||||
|
||||
assert result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is None
|
||||
assert network.waf_policy_reads_failed == {GATEWAY_POLICY_ID.casefold()}
|
||||
assert network.application_gateways_lookup_failed == set()
|
||||
|
||||
def test_missing_waf_policy_is_not_a_failed_read(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=GATEWAY_POLICY_ID)
|
||||
]
|
||||
mock_client.web_application_firewall_policies.get.side_effect = (
|
||||
ResourceNotFoundError("gone")
|
||||
)
|
||||
|
||||
network, result = get_gateways(mock_client)
|
||||
|
||||
assert result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is None
|
||||
assert network.waf_policy_reads_failed == set()
|
||||
|
||||
def test_waf_policy_in_another_subscription_is_not_read(self):
|
||||
other_subscription_policy_id = GATEWAY_POLICY_ID.replace(
|
||||
AZURE_SUBSCRIPTION_ID, "00000000-0000-0000-0000-000000000000"
|
||||
)
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=other_subscription_policy_id)
|
||||
]
|
||||
|
||||
network, result = get_gateways(mock_client)
|
||||
|
||||
mock_client.web_application_firewall_policies.get.assert_not_called()
|
||||
assert result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is None
|
||||
assert network.waf_policy_reads_failed == set()
|
||||
|
||||
def test_waf_policy_without_known_state_leaves_state_unknown(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=GATEWAY_POLICY_ID)
|
||||
]
|
||||
|
||||
_, result = get_gateways(mock_client, [build_waf_policy_model(None)])
|
||||
|
||||
mock_client.web_application_firewall_policies.get.assert_not_called()
|
||||
assert result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is None
|
||||
|
||||
def test_policy_reference_that_is_not_a_waf_policy_id_is_not_read(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=f"{GATEWAY_POLICY_ID}/extra")
|
||||
]
|
||||
|
||||
network, result = get_gateways(mock_client)
|
||||
|
||||
mock_client.web_application_firewall_policies.get.assert_not_called()
|
||||
assert result[AZURE_SUBSCRIPTION_ID][GATEWAY_ID].firewall_policy_enabled is None
|
||||
assert network.waf_policy_reads_failed == set()
|
||||
|
||||
def test_shared_unlisted_waf_policy_is_read_once(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
build_sdk_application_gateway(policy_id=GATEWAY_POLICY_ID),
|
||||
build_sdk_application_gateway(
|
||||
name="agw2", policy_id=GATEWAY_POLICY_ID.upper()
|
||||
),
|
||||
]
|
||||
mock_client.web_application_firewall_policies.get.return_value = (
|
||||
build_sdk_waf_policy("Enabled")
|
||||
)
|
||||
|
||||
_, result = get_gateways(mock_client)
|
||||
|
||||
assert len(result[AZURE_SUBSCRIPTION_ID]) == 2
|
||||
mock_client.web_application_firewall_policies.get.assert_called_once()
|
||||
|
||||
def test_unmappable_gateway_is_skipped_and_marks_subscription(self):
|
||||
broken = azure_network_models.ApplicationGateway(location="westeurope")
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list_all.return_value = [
|
||||
broken,
|
||||
build_sdk_application_gateway(),
|
||||
]
|
||||
|
||||
network, result = get_gateways(mock_client)
|
||||
|
||||
assert list(result[AZURE_SUBSCRIPTION_ID]) == [GATEWAY_ID]
|
||||
assert network.application_gateways_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_failed_resource_group_keeps_gateways_from_the_others(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list.side_effect = [
|
||||
[build_sdk_application_gateway()],
|
||||
Exception("boom"),
|
||||
]
|
||||
|
||||
network, result = get_gateways(
|
||||
mock_client,
|
||||
resource_groups={AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP, "rg2"]},
|
||||
)
|
||||
|
||||
assert list(result[AZURE_SUBSCRIPTION_ID]) == [GATEWAY_ID]
|
||||
assert network.application_gateways_lookup_failed == {AZURE_SUBSCRIPTION_ID}
|
||||
|
||||
def test_missing_resource_group_is_not_a_failed_lookup(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.application_gateways.list.side_effect = ResourceNotFoundError(
|
||||
"gone"
|
||||
)
|
||||
|
||||
network, result = get_gateways(
|
||||
mock_client, resource_groups={AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
|
||||
)
|
||||
|
||||
assert result == {AZURE_SUBSCRIPTION_ID: {}}
|
||||
assert network.application_gateways_lookup_failed == set()
|
||||
@@ -136,60 +136,6 @@ class TestMongodbatlasProvider:
|
||||
with pytest.raises(MongoDBAtlasIdentityError):
|
||||
MongodbatlasProvider.setup_identity(session)
|
||||
|
||||
@patch("requests.get")
|
||||
def test_setup_identity_forbidden_keeps_atlas_error_detail(self, mock_get):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
mock_response.ok = False
|
||||
mock_response.json.return_value = {
|
||||
"detail": "IP address 203.0.113.10 is not allowed to access this resource.",
|
||||
"error": 403,
|
||||
"errorCode": "IP_ADDRESS_NOT_ON_ACCESS_LIST",
|
||||
"reason": "Forbidden",
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
session = MongoDBAtlasSession(
|
||||
public_key=ATLAS_PUBLIC_KEY,
|
||||
private_key=ATLAS_PRIVATE_KEY,
|
||||
)
|
||||
|
||||
with pytest.raises(MongoDBAtlasIdentityError) as exc_info:
|
||||
MongodbatlasProvider.setup_identity(session)
|
||||
|
||||
assert str(exc_info.value) == (
|
||||
"MongoDBAtlasIdentityError[8003]: MongoDB Atlas identity setup failed"
|
||||
" - HTTP 403 IP_ADDRESS_NOT_ON_ACCESS_LIST: IP address 203.0.113.10"
|
||||
" is not allowed to access this resource."
|
||||
)
|
||||
|
||||
@patch("requests.get")
|
||||
def test_setup_identity_error_without_json_body_reports_status(self, mock_get):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 502
|
||||
mock_response.ok = False
|
||||
mock_response.json.side_effect = ValueError("No JSON")
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
session = MongoDBAtlasSession(
|
||||
public_key=ATLAS_PUBLIC_KEY,
|
||||
private_key=ATLAS_PRIVATE_KEY,
|
||||
)
|
||||
|
||||
with pytest.raises(MongoDBAtlasIdentityError) as exc_info:
|
||||
MongodbatlasProvider.setup_identity(session)
|
||||
|
||||
assert exc_info.value.message == (
|
||||
"MongoDB Atlas identity setup failed - HTTP 502"
|
||||
)
|
||||
|
||||
def test_custom_message_does_not_leak_into_later_exceptions(self):
|
||||
MongoDBAtlasIdentityError(message="custom detail")
|
||||
|
||||
assert (
|
||||
MongoDBAtlasIdentityError().message == "MongoDB Atlas identity setup failed"
|
||||
)
|
||||
|
||||
def test_test_connection_success(self):
|
||||
"""Test successful connection test"""
|
||||
with (
|
||||
|
||||
+36
-30
@@ -14,41 +14,47 @@
|
||||
> - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors
|
||||
> - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library
|
||||
> - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks)
|
||||
> - [`prowler-tour`](../skills/prowler-tour/SKILL.md) - Keep product-tour definitions aligned with the UI
|
||||
|
||||
## Auto-invoke Skills
|
||||
|
||||
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
| Action | Skill |
|
||||
| -------------------------------------------------------------- | ------------------- |
|
||||
| Add changelog entry for a PR or feature | `prowler-changelog` |
|
||||
| App Router / Server Actions | `nextjs-16` |
|
||||
| Building AI chat features | `ai-sdk-5` |
|
||||
| Committing changes | `prowler-commit` |
|
||||
| Create PR that requires changelog entry | `prowler-changelog` |
|
||||
| Creating Zod schemas | `zod-4` |
|
||||
| Creating a git commit | `prowler-commit` |
|
||||
| Creating/modifying Prowler UI components | `prowler-ui` |
|
||||
| Fixing bug | `tdd` |
|
||||
| Implementing feature | `tdd` |
|
||||
| Modifying component | `tdd` |
|
||||
| Refactoring code | `tdd` |
|
||||
| Review changelog format and conventions | `prowler-changelog` |
|
||||
| Reviewing Prowler UI components | `prowler-ui` |
|
||||
| Testing hooks or utilities | `vitest` |
|
||||
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
||||
| Using Zustand stores | `zustand-5` |
|
||||
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
|
||||
| Working on task | `tdd` |
|
||||
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
|
||||
| Working with Tailwind classes | `tailwind-4` |
|
||||
| Writing Playwright E2E tests | `playwright` |
|
||||
| Writing Prowler UI E2E tests | `prowler-test-ui` |
|
||||
| Writing React component tests | `vitest` |
|
||||
| Writing React components | `react-19` |
|
||||
| Writing TypeScript types/interfaces | `typescript` |
|
||||
| Writing Vitest tests | `vitest` |
|
||||
| Writing unit tests for UI | `vitest` |
|
||||
| Action | Skill |
|
||||
| ----------------------------------------------------------------- | ------------------- |
|
||||
| Add changelog entry for a PR or feature | `prowler-changelog` |
|
||||
| Adding, updating, or removing a tour definition (\*.tour.ts) | `prowler-tour` |
|
||||
| App Router / Server Actions | `nextjs-16` |
|
||||
| Building AI chat features | `ai-sdk-5` |
|
||||
| Changing button labels or section headings on a tour-covered page | `prowler-tour` |
|
||||
| Committing changes | `prowler-commit` |
|
||||
| Create PR that requires changelog entry | `prowler-changelog` |
|
||||
| Creating Zod schemas | `zod-4` |
|
||||
| Creating a git commit | `prowler-commit` |
|
||||
| Creating/modifying Prowler UI components | `prowler-ui` |
|
||||
| Editing a UI file containing data-tour-id attributes | `prowler-tour` |
|
||||
| Fixing bug | `tdd` |
|
||||
| Implementing feature | `tdd` |
|
||||
| Modifying component | `tdd` |
|
||||
| Refactoring code | `tdd` |
|
||||
| Renaming or removing a data-tour-id attribute value | `prowler-tour` |
|
||||
| Restructuring routes or layouts covered by a tour | `prowler-tour` |
|
||||
| Review changelog format and conventions | `prowler-changelog` |
|
||||
| Reviewing Prowler UI components | `prowler-ui` |
|
||||
| Testing hooks or utilities | `vitest` |
|
||||
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
||||
| Using Zustand stores | `zustand-5` |
|
||||
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
|
||||
| Working on task | `tdd` |
|
||||
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
|
||||
| Working with Tailwind classes | `tailwind-4` |
|
||||
| Writing Playwright E2E tests | `playwright` |
|
||||
| Writing Prowler UI E2E tests | `prowler-test-ui` |
|
||||
| Writing React component tests | `vitest` |
|
||||
| Writing React components | `react-19` |
|
||||
| Writing TypeScript types/interfaces | `typescript` |
|
||||
| Writing Vitest tests | `vitest` |
|
||||
| Writing unit tests for UI | `vitest` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,26 +4,6 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.47.0] (Prowler v5.47.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Icon for the NATO D32 compliance framework on the compliance cards [(#13174)](https://github.com/prowler-cloud/prowler/pull/13174)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Invitations page uses `Status` instead of `State` in the filter, table column and invitation details, and the Invitations and Roles filters show capitalized options [(#13175)](https://github.com/prowler-cloud/prowler/pull/13175)
|
||||
- Product tours and the guided onboarding sequence are no longer shown; new tenants still land on the add-provider wizard [(#13176)](https://github.com/prowler-cloud/prowler/pull/13176)
|
||||
- Add provider wizard connects every provider in one step, saving the account and credentials and testing the connection together, with teammate invitations for every provider and Lighthouse AI help beside the steps [(#13181)](https://github.com/prowler-cloud/prowler/pull/13181)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- User avatar requesting the current page as an image on every load, which rendered each page twice [(#13177)](https://github.com/prowler-cloud/prowler/pull/13177)
|
||||
- Lighthouse AI reply blanking out for a frame when the response finishes streaming, which made the whole text blink and reappear [(#13180)](https://github.com/prowler-cloud/prowler/pull/13180)
|
||||
- Compliance cross-provider tab no longer blanks out when a single catalog page or framework fails to load; it shows the frameworks that loaded with a warning instead [(#13183)](https://github.com/prowler-cloud/prowler/pull/13183)
|
||||
|
||||
---
|
||||
|
||||
## [1.46.0] (Prowler v5.46.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("getComplianceCatalog", () => {
|
||||
eligibleProviderTypes: [],
|
||||
},
|
||||
unavailable: true,
|
||||
incomplete: false,
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -178,7 +177,6 @@ describe("getComplianceCatalog", () => {
|
||||
eligibleProviderTypes: [],
|
||||
},
|
||||
unavailable: true,
|
||||
incomplete: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,7 +199,7 @@ describe("getComplianceCatalog", () => {
|
||||
expect(catalog.entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the loaded pages and flags the catalog as incomplete when a single page fails", async () => {
|
||||
it("flags the catalog as unavailable when a single page fails", async () => {
|
||||
// Given - one of three catalog pages times out
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(
|
||||
@@ -220,8 +218,7 @@ describe("getComplianceCatalog", () => {
|
||||
"cis_1.4_aws",
|
||||
"iso27001_aws",
|
||||
]);
|
||||
expect(catalog.unavailable).toBe(false);
|
||||
expect(catalog.incomplete).toBe(true);
|
||||
expect(catalog.unavailable).toBe(true);
|
||||
});
|
||||
|
||||
it("bounds how many pages it requests at once", async () => {
|
||||
|
||||
@@ -50,7 +50,6 @@ const EMPTY_CATALOG: ComplianceCatalog = {
|
||||
const UNAVAILABLE_CATALOG: ComplianceCatalogLoad = {
|
||||
...EMPTY_CATALOG,
|
||||
unavailable: true,
|
||||
incomplete: false,
|
||||
};
|
||||
|
||||
const GENERIC_ERROR = "Could not update the compliance watchlist.";
|
||||
@@ -141,9 +140,7 @@ export const getComplianceCatalog = async (
|
||||
Math.max(1, firstPage.pageCount),
|
||||
MAX_CATALOG_PAGES,
|
||||
);
|
||||
if (pageCount <= 1) {
|
||||
return { ...firstPage.catalog, unavailable: false, incomplete: false };
|
||||
}
|
||||
if (pageCount <= 1) return { ...firstPage.catalog, unavailable: false };
|
||||
|
||||
const rest: ComplianceCatalog[] = [];
|
||||
let incomplete = firstPage.pageCount > MAX_CATALOG_PAGES;
|
||||
@@ -160,8 +157,7 @@ export const getComplianceCatalog = async (
|
||||
|
||||
return {
|
||||
...mergeCatalogPages([firstPage.catalog, ...rest]),
|
||||
unavailable: false,
|
||||
incomplete,
|
||||
unavailable: incomplete,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching compliance catalog:", error);
|
||||
|
||||
@@ -623,29 +623,6 @@ describe("findings triage actions", () => {
|
||||
expect(createMuteRuleMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return the usage-limit marker when the plan refuses the triage patch", async () => {
|
||||
// Given
|
||||
const { updateFindingTriage } = await importActions();
|
||||
handleApiResponseMock.mockResolvedValue({
|
||||
error: "Payment required",
|
||||
status: 402,
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await updateFindingTriage({
|
||||
findingId: "finding-snapshot-id",
|
||||
findingUid: "finding/stable/uid",
|
||||
triageId: "triage-1",
|
||||
notesCount: 0,
|
||||
status: FINDING_TRIAGE_STATUS.RISK_ACCEPTED,
|
||||
previousStatus: FINDING_TRIAGE_STATUS.OPEN,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({ usageLimited: true });
|
||||
expect(createMuteRuleMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should rollback triage status when automatic muting fails", async () => {
|
||||
// Given
|
||||
const { updateFindingTriage } = await importActions();
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "@/actions/findings/findings-triage.adapter";
|
||||
import { createMuteRule } from "@/actions/mute-rules";
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { ACTION_ERROR_STATUS, UsageLimitError } from "@/lib/action-errors";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
import {
|
||||
FINDING_TRIAGE_STATUS_LABELS,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
type FindingTriageDetail,
|
||||
type FindingTriageLoadedNote,
|
||||
type FindingTriageSummary,
|
||||
type FindingTriageUsageLimitedResult,
|
||||
isMutelistShortcutStatus,
|
||||
type UpdateFindingTriageInput,
|
||||
} from "@/types/findings-triage";
|
||||
@@ -72,15 +70,6 @@ async function getJsonApi(path: `/${string}`) {
|
||||
}
|
||||
|
||||
const throwIfApiError = (result: unknown) => {
|
||||
if (
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
"status" in result &&
|
||||
result.status === ACTION_ERROR_STATUS.PAYMENT_REQUIRED
|
||||
) {
|
||||
throw new UsageLimitError();
|
||||
}
|
||||
|
||||
if (
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
@@ -281,17 +270,6 @@ export async function loadFindingTriageDetail(
|
||||
}
|
||||
|
||||
export async function updateFindingTriage(input: UpdateFindingTriageInput) {
|
||||
try {
|
||||
return await applyFindingTriageUpdate(input);
|
||||
} catch (error) {
|
||||
if (error instanceof UsageLimitError) {
|
||||
return { usageLimited: true } satisfies FindingTriageUsageLimitedResult;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFindingTriageUpdate(input: UpdateFindingTriageInput) {
|
||||
let findingUid: string | undefined;
|
||||
let triagePath: `/${string}`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SentInvitation } from "@/types/invitations";
|
||||
import type { SentInvitation } from "@/types/onboarding-invite";
|
||||
|
||||
const readString = (value: unknown): string | null =>
|
||||
typeof value === "string" && value.length > 0 ? value : null;
|
||||
|
||||
@@ -59,11 +59,11 @@ describe("sendInvite", () => {
|
||||
|
||||
it("forwards the invitation source as a query param", async () => {
|
||||
// When
|
||||
await sendInvite(inviteFormData("provider_connect"));
|
||||
await sendInvite(inviteFormData("onboarding"));
|
||||
|
||||
// Then
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://api.example.com/api/v1/tenants/invitations?source=provider_connect",
|
||||
"https://api.example.com/api/v1/tenants/invitations?source=onboarding",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
const body = JSON.parse(fetchMock.mock.calls[0]?.[1]?.body as string);
|
||||
|
||||
@@ -53,7 +53,7 @@ export const sendInvite = async (formData: FormData) => {
|
||||
const role = formData.get("role");
|
||||
const source = formData.get("source");
|
||||
const url = new URL(`${apiBaseUrl}/tenants/invitations`);
|
||||
// Origin of the invitation (e.g. `provider_connect`); the API may record it.
|
||||
// Origin of the invitation (e.g. `onboarding`); the API may record it.
|
||||
if (typeof source === "string" && source) {
|
||||
url.searchParams.set("source", source);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getRoles } from "@/actions/roles";
|
||||
import type { InvitationRoleOption } from "@/types/invitations";
|
||||
import type { InvitationRoleOption } from "@/types/onboarding-invite";
|
||||
|
||||
const ROLES_PAGE_SIZE = 50;
|
||||
|
||||
|
||||
@@ -170,7 +170,6 @@ describe("issues actions", () => {
|
||||
|
||||
await expect(addIssueNote("issue-1", "note")).resolves.toEqual({
|
||||
error: "You do not have permission.",
|
||||
status: 403,
|
||||
});
|
||||
expect(fetchMock.mock.calls[0][0]).toBe(
|
||||
"https://api.example.com/api/v1/issues/issue-1/triage/notes",
|
||||
|
||||
@@ -128,7 +128,7 @@ export const getIssueFindings = async (
|
||||
}
|
||||
};
|
||||
|
||||
/** Write one issue triage request; any non-2xx comes back as an error to show, with its status. */
|
||||
/** Write one issue triage request; any non-2xx comes back as an error to show. */
|
||||
const writeIssueTriage = async (
|
||||
path: string,
|
||||
method: IssueTriageWriteMethod,
|
||||
@@ -143,9 +143,7 @@ const writeIssueTriage = async (
|
||||
});
|
||||
// The list shows each issue's triage status.
|
||||
const result = await handleApiResponse(response, "/issues");
|
||||
return result?.error
|
||||
? { error: result.error, status: result.status }
|
||||
: { success: true };
|
||||
return result?.error ? { error: result.error } : { success: true };
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
|
||||
@@ -49,9 +49,7 @@ export interface IssueTriageNotesDocument
|
||||
included?: JsonApiResource<Partial<Pick<UserAttributes, "name" | "email">>>[];
|
||||
}
|
||||
|
||||
export type IssueWriteResult =
|
||||
| { success: true }
|
||||
| { error: string; status?: number };
|
||||
export type IssueWriteResult = { success: true } | { error: string };
|
||||
|
||||
export interface UpdateIssueTriageInput {
|
||||
status: FindingTriageManualStatus;
|
||||
|
||||
+21
-2
@@ -58,6 +58,7 @@ vi.mock("@/components/shadcn/table", () => ({
|
||||
data,
|
||||
metadata,
|
||||
controlledPage,
|
||||
getRowAttributes,
|
||||
}: {
|
||||
columns: Array<{
|
||||
id?: string;
|
||||
@@ -74,6 +75,10 @@ vi.mock("@/components/shadcn/table", () => ({
|
||||
};
|
||||
};
|
||||
controlledPage: number;
|
||||
getRowAttributes?: (row: {
|
||||
index: number;
|
||||
original: AttackPathScan;
|
||||
}) => Record<string, string | undefined>;
|
||||
}) => (
|
||||
<div>
|
||||
<span>{metadata.pagination.count} Total Entries</span>
|
||||
@@ -95,8 +100,8 @@ vi.mock("@/components/shadcn/table", () => ({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row) => (
|
||||
<tr key={row.id}>
|
||||
{data.map((row, index) => (
|
||||
<tr key={row.id} {...getRowAttributes?.({ index, original: row })}>
|
||||
{columns.map((column, index) => (
|
||||
<td key={column.id ?? index}>
|
||||
{column.cell
|
||||
@@ -176,6 +181,20 @@ describe("ScanListTable", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("anchors the attack paths scan tour to the first visible scan row", () => {
|
||||
render(
|
||||
<ScanListTable scans={[createScan(1), createScan(2), createScan(3)]} />,
|
||||
);
|
||||
|
||||
const firstRow = screen
|
||||
.getAllByRole("radio", {
|
||||
name: "Select scan",
|
||||
})[0]
|
||||
.closest("tr");
|
||||
|
||||
expect(firstRow).toHaveAttribute("data-tour-id", "attack-paths-scan-list");
|
||||
});
|
||||
|
||||
it("enables the radio button for a failed scan when graph data is ready", async () => {
|
||||
const user = userEvent.setup();
|
||||
const failedScan: AttackPathScan = {
|
||||
|
||||
@@ -295,6 +295,9 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
handleSelectScan(row.original.id);
|
||||
}
|
||||
}}
|
||||
getRowAttributes={(row) =>
|
||||
row.index === 0 ? { "data-tour-id": "attack-paths-scan-list" } : {}
|
||||
}
|
||||
enableRowSelection
|
||||
rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)}
|
||||
/>
|
||||
|
||||
Loaded 100 of 489 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user