Compare commits

...
Author SHA1 Message Date
Pedro Martín c1201dcfd5 fix(pdf): align ENS report requirement status (#10270)
(cherry picked from commit 86daf7bc05)
2026-03-06 11:37:46 +00:00
Prowler Botandprowler-bot bc3cdd492a chore(release): Bump version to v5.19.1 (#10253)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-04 21:19:26 +01:00
Prowler Botandprowler-bot 0366325539 docs: Update version to v5.19.0 (#10257)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-04 21:18:49 +01:00
Prowler Botandprowler-bot 071d6476e2 chore(api): Bump version to v1.20.1 (#10256)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-04 21:18:18 +01:00
Prowler Botandprowler-bot 8573efc53b chore(api): Update prowler dependency to v5.19 for release 5.19.0 (#10250)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-04 12:13:59 +01:00
Pedro Martín 62988821a7 chore(mcp_server): update for release 5.19 (#10248) 2026-03-04 11:46:15 +01:00
Pepe Fagoaga 7a712d5fef chore(changelog): review latest entries (#10246) 2026-03-04 11:26:53 +01:00
Josema CamachoandAndoni A. 8a3d27139a docs: add Attack Paths UI documentation (#10230)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2026-03-04 10:54:45 +01:00
Alejandro Bailo 73415e2f8a chore(ui): improve provider wizard docs link labels (#10244) 2026-03-04 09:33:32 +01:00
Andoni AlonsoandPepe Fagoaga e8d2b4a189 fix(iac): include resource line range in finding UID to prevent duplicates (#10241)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-03 17:40:36 +01:00
Andoni AlonsoandPepe Fagoaga b61b6cba53 feat(sdk): add provider identity fields to OCSF unmapped output (#10240)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-03 16:42:08 +01:00
Pepe Fagoaga 71ee4213b3 chore(ingestions): rename flag, update docs (#10236) 2026-03-03 15:04:34 +01:00
Hugo Pereira BritoandDaniel Barranquero e96ea54f3b feat(m365): add entra_break_glass_users_fido2_security_key_registered security check (#10213)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
2026-03-03 13:58:44 +01:00
Andoni AlonsoandPepe Fagoaga dfca97633e feat(sdk): add provider_uid to OCSF unmapped output (#10231)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-03 13:35:58 +01:00
Daniel BarranqueroandAndoni A. 3538e7accf chore: modify Cloudflare account and resource UIDs (#10227)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2026-03-03 13:09:38 +01:00
Hugo Pereira Brito 548a137046 feat(m365): add entra_authentication_method_sms_voice_disabled security check (#10212) 2026-03-03 13:08:02 +01:00
Daniel BarranqueroandPepe Fagoaga 012fd84cb0 chore: add provider-uid flag for iac provider (#10233)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-03 13:07:15 +01:00
Hugo Pereira BritoandClaude Opus 4.6 8f3e69f571 docs(tutorials): add note about latest scan results in Overview and Resources (#10221)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 12:58:05 +01:00
Pepe Fagoaga 9c2cb5efa8 fix(elbv2): Handle post-quantum (PQ) TLS policies (#10219) 2026-03-03 10:18:00 +01:00
Pepe Fagoaga fa93cabc0b chore: print OCSF import result in the CLI (#10229) 2026-03-03 10:17:04 +01:00
Andoni Alonso efcbbf63c2 docs: review and fix documentation coverage for provider CLI flags (#10040) 2026-03-03 09:57:05 +01:00
Harsh MishraandPepe Fagoaga 150abce4a8 fix(aws): respect AWS_ENDPOINT_URL for STS session creation (#10228)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-03 08:25:59 +01:00
Daniel Barranquero dcf74113fc chore: modify M365 and Github account UIDs (#10226) 2026-03-02 17:22:09 +01:00
mintlify[bot] 42f9b5fb2f docs: rename Findings Ingestion to Import Findings (#10224)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-03-02 16:25:06 +01:00
Alejandro Bailo c74fa131ea fix(ui): navigate to launch step after successful test in update mode (#10223) 2026-03-02 15:59:16 +01:00
Hugo Pereira Brito 07dea4f402 refactor(m365): rename conditional access policy checks to include policy prefix (#10217) 2026-03-02 13:41:24 +01:00
Pepe Fagoaga c71ae75c70 chore(changelog): release v5.19.0 (#10180) 2026-03-02 13:24:03 +01:00
Daniel Barranquero b21ded6d46 feat(openstack): add image service with 6 checks (#10096) 2026-03-02 12:47:49 +01:00
Daniel BarranqueroandAndoni A. 8eddb48b16 feat(openstack): add blockstorage service with 7 checks (#10120)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2026-03-02 12:08:08 +01:00
Daniel BarranqueroandAndoni Alonso d3ba93f0c0 feat(openstack): add networking service with 6 checks (#9970)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2026-03-02 11:55:37 +01:00
191 changed files with 12235 additions and 447 deletions

No files matched your search

+1 -1
View File
@@ -122,7 +122,7 @@ jobs:
"wafv2": ["cognito", "elbv2"],
}
changed_raw = """${STEPS_CHANGED_AWS_OUTPUTS_ALL_CHANGED_FILES}"""
changed_raw = os.environ.get("STEPS_CHANGED_AWS_OUTPUTS_ALL_CHANGED_FILES", "")
# all_changed_files is space-separated, not newline-separated
# Strip leading "./" if present for consistent path handling
changed_files = [Path(f.lstrip("./")) for f in changed_raw.split() if f]
+3 -8
View File
@@ -2,7 +2,7 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.20.0] (Prowler UNRELEASED)
## [1.20.0] (Prowler v5.19.0)
### 🚀 Added
@@ -17,15 +17,9 @@ All notable changes to the **Prowler API** are documented in this file.
- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
- Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992)
- Attack Paths: Add full paths set from [pathfinding.cloud](https://pathfinding.cloud/) [(#10008)](https://github.com/prowler-cloud/prowler/pull/10008)
- Support CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
- Support CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
- Support CSA CCM 4.0 for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
- Support CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057)
- Support CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065)
- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077)
- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
- AI agent guidelines with TDD and testing skills references [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925)
- Attack Paths: Upgrade Cartography from fork 0.126.1 to upstream 0.129.0 and Neo4j driver from 5.x to 6.x [(#10110)](https://github.com/prowler-cloud/prowler/pull/10110)
- Attack Paths: Query results now filtered by provider, preventing future cross-tenant and cross-provider data leakage [(#10118)](https://github.com/prowler-cloud/prowler/pull/10118)
- Attack Paths: Add private labels and properties in Attack Paths graphs for avoiding future overlapping with Cartography's ones [(#10124)](https://github.com/prowler-cloud/prowler/pull/10124)
@@ -35,6 +29,7 @@ All notable changes to the **Prowler API** are documented in this file.
### 🐞 Fixed
- PDF compliance reports consistency with UI: exclude resourceless findings and fix ENS MANUAL status handling [(#10270)](https://github.com/prowler-cloud/prowler/pull/10270)
- Attack Paths: Orphaned temporary Neo4j databases are now cleaned up on scan failure and provider deletion [(#10101)](https://github.com/prowler-cloud/prowler/pull/10101)
- Attack Paths: scan no longer raises `DatabaseError` when provider is deleted mid-scan [(#10116)](https://github.com/prowler-cloud/prowler/pull/10116)
- Tenant compliance summaries recalculated after provider deletion [(#10172)](https://github.com/prowler-cloud/prowler/pull/10172)
@@ -47,7 +42,7 @@ All notable changes to the **Prowler API** are documented in this file.
---
## [1.19.3] (Prowler UNRELEASED)
## [1.19.3] (Prowler v5.18.3)
### 🐞 Fixed
+384 -13
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -24,7 +24,7 @@ dependencies = [
"drf-spectacular-jsonapi==0.5.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.19",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
@@ -49,7 +49,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.20.0"
version = "1.20.1"
[project.scripts]
celery = "src.backend.config.settings.celery"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.20.0
version: 1.20.1
description: |-
Prowler API specification.
+1 -1
View File
@@ -407,7 +407,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.20.0"
spectacular_settings.VERSION = "1.20.1"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
+11 -6
View File
@@ -336,7 +336,6 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
for req in data.requirements:
if req.status == StatusChoices.MANUAL:
continue
m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
if m:
marco = getattr(m, "Marco", "Otros")
@@ -365,9 +364,12 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
elements.append(Paragraph(f"{categoria_name}", self.styles["h3"]))
for req in reqs:
status_indicator = (
"✓" if req["status"] == StatusChoices.PASS else "✗"
)
if req["status"] == StatusChoices.PASS:
status_indicator = "✓"
elif req["status"] == StatusChoices.MANUAL:
status_indicator = "⊙"
else:
status_indicator = "✗"
nivel_badge = f"[{req['nivel'].upper()}]" if req["nivel"] else ""
elements.append(
Paragraph(
@@ -841,11 +843,14 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
elements.append(Spacer(1, 0.15 * inch))
# Status and Nivel badges row
status_color = COLOR_HIGH_RISK # FAIL
status_text = str(req.status).upper()
status_color = (
COLOR_HIGH_RISK if req.status == StatusChoices.FAIL else COLOR_GRAY
)
nivel_color = nivel_colors.get(nivel, COLOR_GRAY)
badges_row1 = [
["State:", "FAIL", "", f"Nivel: {nivel.upper()}"],
["State:", status_text, "", f"Nivel: {nivel.upper()}"],
]
badges_table1 = Table(
badges_row1,
@@ -35,19 +35,27 @@ def _aggregate_requirement_statistics_from_database(
}
"""
requirement_statistics_by_check_id = {}
# TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
aggregated_statistics_queryset = (
Finding.all_objects.filter(
tenant_id=tenant_id, scan_id=scan_id, muted=False
tenant_id=tenant_id,
scan_id=scan_id,
muted=False,
resources__provider__is_deleted=False,
)
.values("check_id")
.annotate(
total_findings=Count(
"id",
distinct=True,
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
),
passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
passed_findings=Count(
"id",
distinct=True,
filter=Q(status=StatusChoices.PASS),
),
)
)
+116 -75
View File
@@ -29,7 +29,7 @@ from tasks.jobs.threatscore_utils import (
_load_findings_for_requirement_checks,
)
from api.models import Finding, StatusChoices
from api.models import Finding, Resource, ResourceFindingMapping, StatusChoices
from prowler.lib.check.models import Severity
matplotlib.use("Agg") # Use non-interactive backend for tests
@@ -39,43 +39,50 @@ matplotlib.use("Agg") # Use non-interactive backend for tests
class TestAggregateRequirementStatistics:
"""Test suite for _aggregate_requirement_statistics_from_database function."""
def _create_finding_with_resource(
self, tenant, scan, uid, check_id, status, severity=Severity.high
):
"""Helper to create a finding linked to a resource (matching scan processing behavior)."""
finding = Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid=uid,
check_id=check_id,
status=status,
severity=severity,
impact=severity,
check_metadata={},
raw_result={},
)
resource = Resource.objects.create(
tenant_id=tenant.id,
provider=scan.provider,
uid=f"resource-{uid}",
name=f"resource-{uid}",
region="us-east-1",
service="test",
type="test::resource",
)
ResourceFindingMapping.objects.create(
tenant_id=tenant.id,
finding=finding,
resource=resource,
)
return finding
def test_aggregates_findings_correctly(self, tenants_fixture, scans_fixture):
"""Verify correct pass/total counts per check are aggregated from database."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
check_id="check_1",
status=StatusChoices.PASS,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant, scan, "finding-2", "check_1", StatusChoices.FAIL
)
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-3",
check_id="check_2",
status=StatusChoices.PASS,
severity=Severity.medium,
impact=Severity.medium,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant, scan, "finding-3", "check_2", StatusChoices.PASS, Severity.medium
)
result = _aggregate_requirement_statistics_from_database(
@@ -106,27 +113,11 @@ class TestAggregateRequirementStatistics:
tenant = tenants_fixture[0]
scan = scans_fixture[0]
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.FAIL
)
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant, scan, "finding-2", "check_1", StatusChoices.FAIL
)
result = _aggregate_requirement_statistics_from_database(
@@ -142,16 +133,12 @@ class TestAggregateRequirementStatistics:
scan = scans_fixture[0]
for i in range(5):
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid=f"finding-{i}",
check_id="check_1",
status=StatusChoices.PASS if i % 2 == 0 else StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant,
scan,
f"finding-{i}",
"check_1",
StatusChoices.PASS if i % 2 == 0 else StatusChoices.FAIL,
)
result = _aggregate_requirement_statistics_from_database(
@@ -162,27 +149,43 @@ class TestAggregateRequirementStatistics:
assert result["check_1"]["total"] == 5
def test_mixed_statuses(self, tenants_fixture, scans_fixture):
"""Verify MANUAL status is counted in total but not passed."""
"""Verify MANUAL status is not counted in total or passed."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
check_id="check_1",
status=StatusChoices.PASS,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
self._create_finding_with_resource(
tenant, scan, "finding-2", "check_1", StatusChoices.MANUAL
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
# MANUAL findings are excluded from the aggregation query
# since it only counts PASS and FAIL statuses
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture):
"""Verify findings without resources are excluded from aggregation."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
# Finding WITH resource → should be counted
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
# Finding WITHOUT resource → should be EXCLUDED
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.MANUAL,
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
@@ -193,8 +196,46 @@ class TestAggregateRequirementStatistics:
str(tenant.id), str(scan.id)
)
# MANUAL findings are excluded from the aggregation query
# since it only counts PASS and FAIL statuses
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
"""Verify a finding with multiple resources is only counted once."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
finding = Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
check_id="check_1",
status=StatusChoices.PASS,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
# Link two resources to the same finding
for i in range(2):
resource = Resource.objects.create(
tenant_id=tenant.id,
provider=scan.provider,
uid=f"resource-{i}",
name=f"resource-{i}",
region="us-east-1",
service="test",
type="test::resource",
)
ResourceFindingMapping.objects.create(
tenant_id=tenant.id,
finding=finding,
resource=resource,
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
+2 -1
View File
@@ -99,7 +99,7 @@
},
"user-guide/tutorials/prowler-app-rbac",
"user-guide/tutorials/prowler-app-api-keys",
"user-guide/tutorials/prowler-app-findings-ingestion",
"user-guide/tutorials/prowler-app-import-findings",
{
"group": "Mutelist",
"expanded": true,
@@ -131,6 +131,7 @@
"user-guide/tutorials/prowler-app-lighthouse-multi-llm"
]
},
"user-guide/tutorials/prowler-app-attack-paths",
"user-guide/tutorials/prowler-cloud-public-ips",
{
"group": "Tutorials",
@@ -121,8 +121,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.18.0"
PROWLER_API_VERSION="5.18.0"
PROWLER_UI_VERSION="5.19.0"
PROWLER_API_VERSION="5.19.0"
```
<Note>
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

@@ -16,23 +16,33 @@ Prowler requires Alibaba Cloud credentials to perform security checks. Authentic
### Credentials URI (Recommended for Centralized Services)
If `--credentials-uri` is provided (or `ALIBABA_CLOUD_CREDENTIALS_URI` environment variable), Prowler will retrieve credentials from the specified external URI endpoint. The URI must return credentials in the standard JSON format.
Prowler can retrieve credentials from an external URI endpoint. Provide the URI via the `--credentials-uri` flag or the `ALIBABA_CLOUD_CREDENTIALS_URI` environment variable. The URI must return credentials in the standard JSON format.
```bash
# Using CLI flag
prowler alibabacloud --credentials-uri http://localhost:8080/credentials
# Or using environment variable
export ALIBABA_CLOUD_CREDENTIALS_URI="http://localhost:8080/credentials"
prowler alibabacloud
```
### OIDC Role Authentication (Recommended for ACK/Kubernetes)
If OIDC environment variables are set, Prowler will use OIDC authentication to assume the specified role. This is the most secure method for containerized applications running in ACK (Alibaba Container Service for Kubernetes) with RRSA enabled.
OIDC authentication assumes the specified role using an OIDC token. This is the most secure method for containerized applications running in ACK (Alibaba Container Service for Kubernetes) with RRSA enabled.
The role ARN can be provided via the `--oidc-role-arn` flag or the `ALIBABA_CLOUD_ROLE_ARN` environment variable. The OIDC provider ARN and token file must be set via environment variables:
Required environment variables:
- `ALIBABA_CLOUD_ROLE_ARN`
- `ALIBABA_CLOUD_OIDC_PROVIDER_ARN`
- `ALIBABA_CLOUD_OIDC_TOKEN_FILE`
```bash
# Using CLI flag for role ARN
export ALIBABA_CLOUD_OIDC_PROVIDER_ARN="acs:ram::123456789012:oidc-provider/ack-rrsa-provider"
export ALIBABA_CLOUD_OIDC_TOKEN_FILE="/var/run/secrets/tokens/oidc-token"
prowler alibabacloud --oidc-role-arn acs:ram::123456789012:role/YourRole
# Or using all environment variables
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/YourRole"
export ALIBABA_CLOUD_OIDC_PROVIDER_ARN="acs:ram::123456789012:oidc-provider/ack-rrsa-provider"
export ALIBABA_CLOUD_OIDC_TOKEN_FILE="/var/run/secrets/tokens/oidc-token"
@@ -54,9 +64,17 @@ prowler alibabacloud
### RAM Role Assumption (Recommended for Cross-Account)
For cross-account access, use RAM role assumption. You must provide the initial credentials (access keys) and the target role ARN.
For cross-account access, use RAM role assumption. Provide the initial credentials (access keys) via environment variables and the target role ARN via the `--role-arn` flag or the `ALIBABA_CLOUD_ROLE_ARN` environment variable.
The `--role-session-name` flag customizes the session identifier (defaults to `ProwlerAssessmentSession`).
```bash
# Using CLI flags
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
prowler alibabacloud --role-arn acs:ram::123456789012:role/ProwlerAuditRole --role-session-name MyAuditSession
# Or using all environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/ProwlerAuditRole"
@@ -117,6 +117,12 @@ prowler alibabacloud
#### RAM Role Assumption
```bash
# Using --role-arn CLI flag
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
prowler alibabacloud --role-arn acs:ram::123456789012:role/ProwlerAuditRole
# Or using environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/ProwlerAuditRole"
@@ -135,3 +135,16 @@ prowler gcp --impersonate-service-account <service-account-email>
```
More details on authentication methods in the [Authentication](/user-guide/providers/gcp/authentication) page.
### Skip API Check
By default, Prowler verifies which Google Cloud APIs are enabled before running checks for each service. To skip this verification and assume all APIs are active, use the `--skip-api-check` flag:
```console
prowler gcp --skip-api-check
```
<Note>
This is useful when the authenticated principal lacks the `serviceusage.services.list` permission but has access to individual service APIs.
</Note>
@@ -135,7 +135,7 @@ prowler iac --scan-path ./my-iac-directory --scanners vuln misconfig
#### Exclude Paths
```sh
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test ./my-iac-directory/examples
```
### Output
@@ -127,4 +127,18 @@ Include PowerShell module initialization to run every check:
prowler m365 --sp-env-auth --init-modules
```
### Region Selection
By default, Prowler connects to the global Microsoft 365 environment (`M365Global`). To target a different cloud environment, use the `--region` flag:
```console
prowler m365 --sp-env-auth --region M365USGovernment
```
Available regions:
* **M365Global** (default): Standard commercial cloud
* **M365China**: China-operated cloud (21Vianet)
* **M365USGovernment**: US Government cloud (GCC High)
---
@@ -0,0 +1,203 @@
---
title: "Attack Paths"
description: "Identify privilege escalation chains and security misconfigurations across cloud environments using graph-based analysis."
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.17.0" />
Attack Paths analyzes relationships between cloud resources, permissions, and security findings to detect how privileges can be escalated and how misconfigurations can be exploited by threat actors.
By mapping these relationships as a graph, Attack Paths reveals risks that individual security checks cannot detect on their own — such as an IAM role that can escalate its own permissions, or a chain of policies that grants unintended access to sensitive resources.
<Note>
Attack Paths is currently available for **AWS** providers. Support for
additional providers is planned.
</Note>
## Prerequisites
The following prerequisites are required for Attack Paths:
- **An AWS provider is configured** with valid credentials in Prowler App. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
- **At least one scan has completed** on the configured AWS provider. Attack Paths scans run automatically alongside regular security scans — no separate configuration is required.
## How Attack Paths Scans Work
Attack Paths scans are generated automatically when a security scan runs on an AWS provider. Each completed scan produces graph data that maps relationships between IAM principals, policies, trust configurations, and other resources.
Once the scan finishes and the graph data is ready, the scan appears in the Attack Paths scan table with a **Completed** status. Scans that are still processing display as **Executing** or **Scheduled**.
<Note>
Since Prowler scans all configured providers every **24 hours** by default,
Attack Paths data stays up to date automatically.
</Note>
## Accessing Attack Paths
To open Attack Paths, click **Attack Paths** in the left navigation menu.
<img
src="/images/prowler-app/attack-paths/navigation.png"
alt="Attack Paths navigation menu entry"
width="700"
/>
The main interface is divided into two areas:
- **Left panel:** A table listing all available Attack Paths scans
- **Right panel:** The query selector, parameter form, and execute controls
## Selecting a Scan
The scans table displays all Attack Paths scans with the following columns:
- **Provider / Account:** The AWS provider alias and account identifier
- **Last scan date:** When the scan completed
- **Status:** Current state of the scan (Completed, Executing, Scheduled, or Failed)
- **Progress:** Completion percentage for in-progress scans
- **Duration:** Total scan time
To select a scan for analysis, click **Select** on any row with a **Completed** status.
<img
src="/images/prowler-app/attack-paths/scan-list-table.png"
alt="Attack Paths scan list table showing completed scans"
width="700"
/>
<Note>
Only scans with a **Completed** status and ready graph data can be selected.
Scans that are still executing or have failed appear with disabled action
buttons.
</Note>
## Choosing a Query
After selecting a scan, the right panel activates a query dropdown. Each query targets a specific type of privilege escalation or misconfiguration pattern.
To choose a query, click the dropdown and select from the available options. Each option displays:
- **Query name:** A descriptive title (e.g., "IAM Privilege Escalation via AssumeRole")
- **Short description:** A brief summary of what the query detects
<img
src="/images/prowler-app/attack-paths/query-selector.png"
alt="Attack Paths query selector dropdown showing available queries"
width="700"
/>
Once selected, a description card appears below the dropdown with additional context about the query, including attribution links to external references when available.
## Configuring Query Parameters
Some queries accept optional or required parameters to narrow the scope of the analysis. When a query has parameters, a dynamic form appears below the query description.
- **Required fields** are marked with an asterisk (\*) and must be filled before executing
- **Optional fields** refine the query results but are not mandatory
If a query requires no parameters, the form displays a message confirming that the query is ready to execute.
<img
src="/images/prowler-app/attack-paths/query-parameters.png"
alt="Attack Paths query parameter form with fields"
width="700"
/>
## Executing a Query
To run the selected query against the scan data, click **Execute Query**. The button displays a loading state while the query processes.
If the query returns no results, an informational message appears. Common reasons include:
- **No matching patterns found:** The scanned environment does not contain the privilege escalation chain the query targets
- **Insufficient permissions:** The scan credentials may not have captured all the data the query needs
<img
src="/images/prowler-app/attack-paths/execute-query.png"
alt="Attack Paths right panel with query selected and execute button"
width="700"
/>
## Exploring the Graph
After a successful execution, the graph visualization renders below the query builder in a full-width panel. The graph maps relationships between cloud resources, IAM entities, and security findings.
### Node Types
- **Resource nodes** (rounded pills): Represent cloud resources such as IAM roles, policies, EC2 instances, and S3 buckets. Each resource type has a distinct color.
- **Finding nodes** (hexagons): Represent Prowler security findings linked to resources in the graph. Colors indicate severity level (critical, high, medium, low).
### Edge Types
- **Solid lines:** Direct relationships between resources (e.g., a role attached to a policy)
- **Dashed lines:** Connections between resources and their associated findings
A **legend** at the bottom of the graph lists all node types and edge types present in the current view.
<img
src="/images/prowler-app/attack-paths/graph-visualization.png"
alt="Attack Paths graph showing nodes, edges, and legend"
width="700"
/>
## Interacting with the Graph
### Filtering by Node
Click any node in the graph to filter the view and display only paths that pass through that node. When a filter is active:
- An information banner shows which node is selected
- Click **Back to Full View** to restore the complete graph
<img
src="/images/prowler-app/attack-paths/graph-filtered.png"
alt="Attack Paths graph filtered to show paths through a selected node"
width="700"
/>
### Graph Controls
The toolbar in the top-right corner of the graph provides:
- **Zoom in / Zoom out:** Adjust the zoom level
- **Fit to screen:** Reset the view to fit all nodes
- **Export:** Download the current graph as an SVG file
- **Fullscreen:** Open the graph in a full-screen modal with a side-by-side node detail panel
<Note>
Use **Ctrl + Scroll** (or **Cmd + Scroll** on macOS) to zoom directly within
the graph area.
</Note>
## Viewing Node Details
Click any node to open the **Node Details** panel below the graph. This panel displays:
- **Node type:** The resource category (e.g., "IAM Role," "EC2 Instance")
- **Properties:** All attributes of the selected node, including identifiers, timestamps, and configuration details
- **Related findings** (for resource nodes): A list of Prowler findings linked to the resource, with severity, title, and status
- **Affected resources** (for finding nodes): A list of resources associated with the finding
For finding nodes, a "View Finding" button links directly to the finding detail page for further investigation.
<img
src="/images/prowler-app/attack-paths/node-details.png"
alt="Attack Paths node detail panel showing properties and related findings"
width="700"
/>
## Fullscreen Mode
To expand the graph for detailed exploration, click the fullscreen icon in the graph toolbar. The fullscreen modal provides:
- The full graph visualization with all zoom and export controls
- A side panel for node details that appears when a node is selected
- All filtering and interaction capabilities available in the standard view
<img
src="/images/prowler-app/attack-paths/fullscreen-mode.png"
alt="Attack Paths fullscreen mode with graph and node detail side panel"
width="700"
/>
@@ -1,5 +1,5 @@
---
title: 'Findings Ingestion'
title: 'Import Findings'
description: 'Upload OCSF scan results to Prowler Cloud from external sources or the CLI'
---
@@ -10,7 +10,7 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
Findings Ingestion enables uploading OCSF (Open Cybersecurity Schema Framework) scan results to Prowler Cloud. This feature supports importing findings from Prowler CLI output files that use the [Detection Finding](https://schema.ocsf.io/classes/detection_finding) class.
<Note>
This feature is available exclusively in **Prowler Cloud**.
This feature is available exclusively in **Prowler Cloud** with a paid subscription.
</Note>
## OCSF Detection Finding format
@@ -132,69 +132,77 @@ Only **Detection Finding** (`class_uid: 2004`) records are accepted. Other OCSF
## Required permissions
The **Manage Ingestions** RBAC permission controls access to the ingestion endpoints. Without this permission, findings cannot be submitted via the API or `--export-ocsf`.
The **Manage Ingestions** RBAC permission controls access to the ingestion endpoints. Without this permission, findings cannot be submitted via the API or `--push-to-cloud`.
For more information about RBAC permissions, refer to the [Prowler App RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
## Using the CLI
The `--export-ocsf` flag uploads scan results directly to Prowler Cloud after a scan completes. This approach automates the ingestion process without manual file uploads.
The `--push-to-cloud` flag uploads scan results directly to Prowler Cloud after a scan completes. This approach automates the ingestion process without manual file uploads.
### Prerequisites
- A valid Prowler Cloud API key (see [API Keys](/user-guide/tutorials/prowler-app-api-keys))
- The `PROWLER_API_KEY` environment variable configured
- The `PROWLER_CLOUD_API_KEY` environment variable configured
### Basic usage
```bash
export PROWLER_API_KEY="pk_your_api_key_here"
export PROWLER_CLOUD_API_KEY="pk_your_api_key_here"
prowler aws --export-ocsf
prowler aws --push-to-cloud
```
### Combining with output formats
When using `--export-ocsf` with custom output formats that exclude OCSF, Prowler generates a temporary OCSF file for upload:
When using `--push-to-cloud` with custom output formats that exclude OCSF, Prowler generates a temporary OCSF file for upload:
The temporary OCSF file is saved in the system temporary directory and not in the output path passed with `-o`.
```bash
prowler aws --services accessanalyzer -M csv --export-ocsf -o /tmp/scan-output
prowler aws --services accessanalyzer -M csv --push-to-cloud -o /tmp/scan-output
```
When default output formats include OCSF, Prowler reuses the existing file. Default output formats include JSON-OCSF:
```bash
prowler aws --services accessanalyzer --export-ocsf -o /tmp/scan-output
prowler aws --services accessanalyzer --push-to-cloud -o /tmp/scan-output
```
### CLI output examples
**Successful upload:**
```
Exporting OCSF to Prowler Cloud, please wait...
Pushing findings to Prowler Cloud, please wait...
OCSF export accepted. Ingestion job: fa8bc8c5-4925-46a0-9fe0-f6575905e094
Findings successfully pushed to Prowler Cloud. Ingestion job: fa8bc8c5-4925-46a0-9fe0-f6575905e094
See more details here: https://cloud.prowler.com/scans
```
**Missing API key:**
```
WARNING: OCSF export skipped: no API key configured. Set the PROWLER_API_KEY
Push to Prowler Cloud skipped: no API key configured. Set the PROWLER_CLOUD_API_KEY
environment variable to enable it. Scan results were saved to
/tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
**API unreachable:**
```
WARNING: OCSF export skipped: could not reach the Prowler Cloud API at
Push to Prowler Cloud failed: could not reach the Prowler Cloud API at
https://api.prowler.com. Check the URL and your network connection. Scan results
were saved to /tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
**No subscription:**
```
Push to Prowler Cloud failed: this feature is only available with a Prowler Cloud
subscription. Scan results were saved to
/tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
**Invalid API key:**
```
WARNING: OCSF export failed: the API returned HTTP 401. Verify your API key is
Push to Prowler Cloud failed: the API returned HTTP 401. Verify your API key is
valid and has the right permissions. Scan results were saved to
/tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
@@ -212,10 +220,10 @@ The Ingestion API provides endpoints for submitting OCSF files and monitoring jo
Include the API key in the `Authorization` header:
```bash
export PROWLER_API_KEY="pk_your_api_key_here"
export PROWLER_CLOUD_API_KEY="pk_your_api_key_here"
curl -X POST \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-F "file=@/path/to/findings.ocsf.json" \
https://api.prowler.com/api/v1/ingestions
```
@@ -229,7 +237,7 @@ Upload a `.ocsf.json` file containing a JSON array of OCSF Detection Finding rec
**Request:**
```bash
curl -X POST \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-F "file=@scan-results.ocsf.json" \
https://api.prowler.com/api/v1/ingestions
```
@@ -267,7 +275,7 @@ Monitor the progress of an ingestion job.
**Request:**
```bash
curl -X GET \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Accept: application/vnd.api+json" \
https://api.prowler.com/api/v1/ingestions/3650fef9-8e5f-4808-a95f-74f0afae8499
```
@@ -319,7 +327,7 @@ Retrieve a list of ingestion jobs for the tenant.
**Request:**
```bash
curl -X GET \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Accept: application/vnd.api+json" \
"https://api.prowler.com/api/v1/ingestions?filter[status]=completed&page[size]=10"
```
@@ -333,7 +341,7 @@ Retrieve error details for a specific ingestion job.
**Request:**
```bash
curl -X GET \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Accept: application/vnd.api+json" \
https://api.prowler.com/api/v1/ingestions/3650fef9-8e5f-4808-a95f-74f0afae8499/errors
```
@@ -363,9 +371,9 @@ Prowler must be installed in the CI/CD environment before running scans. Refer t
- name: Run Prowler and upload to Cloud
env:
PROWLER_API_KEY: ${{ secrets.PROWLER_API_KEY }}
PROWLER_CLOUD_API_KEY: ${{ secrets.PROWLER_CLOUD_API_KEY }}
run: |
prowler aws --services s3,iam --export-ocsf
prowler aws --services s3,iam --push-to-cloud
```
### GitLab CI
@@ -374,9 +382,9 @@ Prowler must be installed in the CI/CD environment before running scans. Refer t
prowler_scan:
script:
- pip install prowler
- prowler aws --services s3,iam --export-ocsf
- prowler aws --services s3,iam --push-to-cloud
variables:
PROWLER_API_KEY: $PROWLER_API_KEY
PROWLER_CLOUD_API_KEY: $PROWLER_CLOUD_API_KEY
```
## Billing impact
@@ -392,6 +400,23 @@ For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
## Troubleshooting
### "Push to Prowler Cloud skipped: no API key configured"
- Set the `PROWLER_CLOUD_API_KEY` environment variable before running the scan
- Verify the variable is exported and not empty
### "Push to Prowler Cloud failed: could not reach the Prowler Cloud API"
- Verify network connectivity to `api.prowler.com`
- Check firewall rules allow outbound HTTPS traffic
- Confirm the API endpoint is not blocked by proxy settings
- If using a custom base URL via `PROWLER_CLOUD_API_BASE_URL`, verify it is correct
### "Push to Prowler Cloud failed: this feature is only available with a Prowler Cloud subscription"
- The API returned HTTP 402, meaning your tenant does not have an active subscription
- Visit [Prowler Cloud Pricing](https://prowler.com/pricing) to review available plans
### HTTP 401 Unauthorized
- Verify the API key is valid and not revoked
@@ -408,9 +433,3 @@ For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
- Check the `/api/v1/ingestions/{id}/errors` endpoint for details
- Verify the OCSF file format is valid
- Ensure the file contains Detection Finding records
### CLI reports "could not reach the Prowler Cloud API"
- Verify network connectivity to `api.prowler.com`
- Check firewall rules allow outbound HTTPS traffic
- Confirm the API endpoint is not blocked by proxy settings
@@ -238,6 +238,6 @@ To grant all administrative permissions, select the **Grant all admin permission
The following permissions are available exclusively in **Prowler Cloud**:
**Manage Ingestions:** Submit and manage findings ingestion jobs via the API. Required to upload OCSF scan results using the `--export-ocsf` CLI flag or the ingestion endpoints. See [Findings Ingestion](/user-guide/tutorials/prowler-app-findings-ingestion) for details.
**Manage Ingestions:** Submit and manage findings ingestion jobs via the API. Required to upload OCSF scan results using the `--push-to-cloud` CLI flag or the ingestion endpoints. See [Import Findings](/user-guide/tutorials/prowler-app-import-findings) for details.
**Manage Billing:** Access and manage billing settings, subscription plans, and payment methods.
+12
View File
@@ -134,6 +134,18 @@ Track the progress of your scan in the `Scans` section:
<img src="/images/scan-progress.png" alt="Scan Progress" width="700" />
<Note>
**How Dashboards Display Scan Data**
Each dashboard handles scan data differently:
* **Overview** displays aggregated metrics from the **latest completed scan per provider** only.
* **Findings** displays results from the **latest completed scan per provider** by default. To access historical findings, apply a date or scan filter.
* **Resources** lists **all discovered resources across all scans**. However, when selecting a resource, the Findings tab within the resource detail shows only findings from the **latest completed scan**. If the latest scan did not evaluate a particular resource, its Findings tab may appear empty.
When a new scan completes or a new data ingestion is processed, the dashboards automatically reflect the updated results.
</Note>
## **Step 8: Analyze the Findings**
While the scan is running, start exploring the findings in these sections:
+28 -22
View File
@@ -2,52 +2,58 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.4.0] (Prowler UNRELEASED)
## [0.4.0] (Prowler v5.19.0)
- Add new MCP Server tools for Prowler Attack Paths [(#10145)](https://github.com/prowler-cloud/prowler/pull/10145)
### 🚀 Added
- Attack Paths tools to list scans, discover queries, and run Cypher queries against Neo4j [(#10145)](https://github.com/prowler-cloud/prowler/pull/10145)
---
## [0.3.0] (Prowler v5.16.0)
### Added
### 🚀 Added
- Add new MCP Server tools for Prowler Compliance Framework Management [(#9568)](https://github.com/prowler-cloud/prowler/pull/9568)
- MCP Server tools for Prowler Compliance Framework Management [(#9568)](https://github.com/prowler-cloud/prowler/pull/9568)
### Changed
### 🔄 Changed
- Update API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9542)
- Standardize Prowler Hub and Docs tools format for AI optimization [(#9578)](https://github.com/prowler-cloud/prowler/pull/9578)
- API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9542)
- Prowler Hub and Docs tools format standardized for AI optimization [(#9578)](https://github.com/prowler-cloud/prowler/pull/9578)
---
## [0.2.0] (Prowler v5.15.0)
### Added
### 🚀 Added
- Remove all Prowler App MCP tools; and add new MCP Server tools for Prowler Findings and Compliance [(#9300)](https://github.com/prowler-cloud/prowler/pull/9300)
- Add new MCP Server tools for Prowler Providers Management [(#9350)](https://github.com/prowler-cloud/prowler/pull/9350)
- Add new MCP Server tools for Prowler Resources Management [(#9380)](https://github.com/prowler-cloud/prowler/pull/9380)
- Add new MCP Server tools for Prowler Scans Management [(#9509)](https://github.com/prowler-cloud/prowler/pull/9509)
- Add new MCP Server tools for Prowler Muting Management [(#9510)](https://github.com/prowler-cloud/prowler/pull/9510)
- MCP Server tools for Prowler Findings and Compliance, replacing all Prowler App MCP tools [(#9300)](https://github.com/prowler-cloud/prowler/pull/9300)
- MCP Server tools for Prowler Providers Management [(#9350)](https://github.com/prowler-cloud/prowler/pull/9350)
- MCP Server tools for Prowler Resources Management [(#9380)](https://github.com/prowler-cloud/prowler/pull/9380)
- MCP Server tools for Prowler Scans Management [(#9509)](https://github.com/prowler-cloud/prowler/pull/9509)
- MCP Server tools for Prowler Muting Management [(#9510)](https://github.com/prowler-cloud/prowler/pull/9510)
---
## [0.1.1] (Prowler v5.14.0)
### Fixed
### 🐞 Fixed
- Fix documentation MCP Server to return list of dictionaries [(#9205)](https://github.com/prowler-cloud/prowler/pull/9205)
- Documentation MCP Server to return list of dictionaries [(#9205)](https://github.com/prowler-cloud/prowler/pull/9205)
---
## [0.1.0] (Prowler v5.13.0)
### Added
### 🚀 Added
- Initial release of Prowler MCP Server [(#8695)](https://github.com/prowler-cloud/prowler/pull/8695)
- Set appropiate user-agent in requests [(#8724)](https://github.com/prowler-cloud/prowler/pull/8724)
- Appropriate user-agent in requests [(#8724)](https://github.com/prowler-cloud/prowler/pull/8724)
- Basic logger functionality [(#8740)](https://github.com/prowler-cloud/prowler/pull/8740)
- Add new MCP Server for Prowler Cloud and Prowler App (Self-Managed) APIs [(#8744)](https://github.com/prowler-cloud/prowler/pull/8744)
- MCP Server for Prowler Cloud and Prowler App (Self-Managed) APIs [(#8744)](https://github.com/prowler-cloud/prowler/pull/8744)
- HTTP transport support [(#8784)](https://github.com/prowler-cloud/prowler/pull/8784)
- Add new MCP Server for Prowler Documentation [(#8795)](https://github.com/prowler-cloud/prowler/pull/8795)
- MCP Server for Prowler Documentation [(#8795)](https://github.com/prowler-cloud/prowler/pull/8795)
- API key support for STDIO mode and enhanced HTTP mode authentication [(#8823)](https://github.com/prowler-cloud/prowler/pull/8823)
- Add health check endpoint [(#8905)](https://github.com/prowler-cloud/prowler/pull/8905)
- Update Prowler Documentation MCP Server to use Mintlify API [(#8916)](https://github.com/prowler-cloud/prowler/pull/8916)
- Add custom production deployment using uvicorn [(#8958)](https://github.com/prowler-cloud/prowler/pull/8958)
- Health check endpoint [(#8905)](https://github.com/prowler-cloud/prowler/pull/8905)
- Prowler Documentation MCP Server updated to use Mintlify API [(#8916)](https://github.com/prowler-cloud/prowler/pull/8916)
- Custom production deployment using uvicorn [(#8958)](https://github.com/prowler-cloud/prowler/pull/8958)
+23 -18
View File
@@ -2,40 +2,47 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.19.0] (Prowler UNRELEASED)
## [5.19.0] (Prowler v5.19.0)
### 🚀 Added
- `entra_default_app_management_policy_enabled` check for M365 provider [(#9898)](https://github.com/prowler-cloud/prowler/pull/9898)
- `entra_authentication_method_sms_voice_disabled` check for M365 provider [(#10212)](https://github.com/prowler-cloud/prowler/pull/10212)
- `Google Workspace` provider support with Directory service including 1 security check [(#10022)](https://github.com/prowler-cloud/prowler/pull/10022)
- `entra_app_enforced_restrictions` check for M365 provider [(#10058)](https://github.com/prowler-cloud/prowler/pull/10058)
- `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
- `entra_conditional_access_policy_app_enforced_restrictions` check for M365 provider [(#10058)](https://github.com/prowler-cloud/prowler/pull/10058)
- `entra_app_registration_no_unused_privileged_permissions` check for M365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)
- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)
- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832)
- AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975)
- CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
- CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
- CSA CCM 4.0 for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
- CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057)
- OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020)
- `image` provider for container image scanning with Trivy integration [(#9984)](https://github.com/prowler-cloud/prowler/pull/9984)
- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944)
- CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
- ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066)
- `--export-ocsf` CLI flag to upload OCSF scan results to Prowler Cloud [(#10095)](https://github.com/prowler-cloud/prowler/pull/10095)
- `scan_id` field in OCSF `unmapped` output for ingestion correlation [(#10095)](https://github.com/prowler-cloud/prowler/pull/10095)
- `defenderxdr_endpoint_privileged_user_exposed_credentials` check for M365 provider [(#10084)](https://github.com/prowler-cloud/prowler/pull/10084)
- `defenderxdr_critical_asset_management_pending_approvals` check for M365 provider [(#10085)](https://github.com/prowler-cloud/prowler/pull/10085)
- `entra_seamless_sso_disabled` check for m365 provider [(#10086)](https://github.com/prowler-cloud/prowler/pull/10086)
- `entra_seamless_sso_disabled` check for M365 provider [(#10086)](https://github.com/prowler-cloud/prowler/pull/10086)
- Registry scan mode for `image` provider: enumerate and scan all images from OCI standard, Docker Hub, and ECR [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985)
- Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)
- File descriptor limits (`ulimits`) for Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)
- SecNumCloud compliance framework for the AWS provider [(#10117)](https://github.com/prowler-cloud/prowler/pull/10117)
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
- `entra_require_mfa_for_management_api` check for m365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150)
- `entra_conditional_access_policy_require_mfa_for_management_api` check for M365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150)
- OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135)
- `entra_break_glass_account_fido2_security_key_registered` check for M365 provider [(#10213)](https://github.com/prowler-cloud/prowler/pull/10213)
- `entra_default_app_management_policy_enabled` check for M365 provider [(#9898)](https://github.com/prowler-cloud/prowler/pull/9898)
- OpenStack networking service with 6 security checks [(#9970)](https://github.com/prowler-cloud/prowler/pull/9970)
- OpenStack block storage service with 7 security checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120)
- OpenStack compute service with 7 security checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944)
- OpenStack image service with 6 security checks [(#10096)](https://github.com/prowler-cloud/prowler/pull/10096)
- `--provider-uid` CLI flag for IaC provider, used as `cloud.account.uid` in OCSF output and required with `--export-ocsf` [(#10233)](https://github.com/prowler-cloud/prowler/pull/10233)
- `unmapped.provider_uid` field in OCSF output to match CLI scan results with API provider entities during ingestion [(#10231)](https://github.com/prowler-cloud/prowler/pull/10231)
- `unmapped.provider` field in OCSF output for provider name availability in non-cloud providers like Kubernetes [(#10240)](https://github.com/prowler-cloud/prowler/pull/10240)
### 🔄 Changed
@@ -58,13 +65,20 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update Azure Entra ID service metadata to new format [(#9619)](https://github.com/prowler-cloud/prowler/pull/9619)
- Update Azure Virtual Machines service metadata to new format [(#9629)](https://github.com/prowler-cloud/prowler/pull/9629)
- Cloudflare provider credential validation with specific exceptions [(#9910)](https://github.com/prowler-cloud/prowler/pull/9910)
- Enhance AWS IAM privilege escalation detection with patterns from pathfinding.cloud library [(#9922)](https://github.com/prowler-cloud/prowler/pull/9922)
- Bump Trivy from 0.66.0 to 0.69.2 [(#10210)](https://github.com/prowler-cloud/prowler/pull/10210)
- Standardize GitHub and M365 provider account UIDs for consistent OCSF output [(#10226)](https://github.com/prowler-cloud/prowler/pull/10226)
- Standardize Cloudflare account and resource UIDs to prevent None values in findings [(#10227)](https://github.com/prowler-cloud/prowler/pull/10227)
### 🐞 Fixed
- Update AWS checks metadata URLs to replace deprecated Trend Micro CloudOne Conformity (EOL July 2026) with Vision One and remove docs.prowler.com references [(#10068)](https://github.com/prowler-cloud/prowler/pull/10068)
- Standardize resource_id values across Azure checks to use actual Azure resource IDs and prevent duplicate resource entries [(#9994)](https://github.com/prowler-cloud/prowler/pull/9994)
- VPC endpoint service collection filtering third-party services that caused AccessDenied errors on `DescribeVpcEndpointServicePermissions` [(#10152)](https://github.com/prowler-cloud/prowler/pull/10152)
- Handle serialization errors in OCSF output for non-serializable resource metadata [(#10129)](https://github.com/prowler-cloud/prowler/pull/10129)
- Respect `AWS_ENDPOINT_URL` environment variable for STS session creation [(#10228)](https://github.com/prowler-cloud/prowler/pull/10228)
- Help text and typos in CLI flags [(#10040)](https://github.com/prowler-cloud/prowler/pull/10040)
- `elbv2_insecure_ssl_ciphers` false positive on AWS post-quantum (PQ) TLS policies like `ELBSecurityPolicy-TLS13-1-2-PQ-2025-09` [(#10219)](https://github.com/prowler-cloud/prowler/pull/10219)
### 🔐 Security
@@ -73,14 +87,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.18.4] (Prowler v5.18.4)
### 🐞 Fixed
- Handle serialization errors in OCSF output for non-serializable resource metadata [(#10129)](https://github.com/prowler-cloud/prowler/pull/10129)
---
## [5.18.3] (Prowler v5.18.3)
### 🐞 Fixed
@@ -210,7 +216,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update Azure AI Search service metadata to new format [(#9087)](https://github.com/prowler-cloud/prowler/pull/9087)
- Update Azure AKS service metadata to new format [(#9611)](https://github.com/prowler-cloud/prowler/pull/9611)
- Update Azure API Management service metadata to new format [(#9612)](https://github.com/prowler-cloud/prowler/pull/9612)
- Enhance AWS IAM privilege escalation detection with patterns from pathfinding.cloud library [(#9922)](https://github.com/prowler-cloud/prowler/pull/9922)
### Fixed
+27 -19
View File
@@ -529,7 +529,7 @@ def prowler():
provider=global_provider, stats=stats
)
if getattr(args, "export_ocsf", False):
if getattr(args, "push_to_cloud", False):
if not ocsf_output or not getattr(ocsf_output, "file_path", None):
tmp_ocsf = tempfile.NamedTemporaryFile(
suffix=json_ocsf_file_suffix, delete=False
@@ -541,42 +541,50 @@ def prowler():
tmp_ocsf.close()
ocsf_output.batch_write_data_to_file()
print(
f"{Style.BRIGHT}\nExporting OCSF to Prowler Cloud, please wait...{Style.RESET_ALL}"
f"{Style.BRIGHT}\nPushing findings to Prowler Cloud, please wait...{Style.RESET_ALL}"
)
try:
response = send_ocsf_to_api(ocsf_output.file_path)
except ValueError:
logger.warning(
"OCSF export skipped: no API key configured. "
"Set the PROWLER_API_KEY environment variable to enable it. "
f"Scan results were saved to {ocsf_output.file_path}"
print(
f"{Style.BRIGHT}{Fore.YELLOW}\nPush to Prowler Cloud skipped: no API key configured. "
"Set the PROWLER_CLOUD_API_KEY environment variable to enable it. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
except requests.ConnectionError:
logger.warning(
"OCSF export skipped: could not reach the Prowler Cloud API at "
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: could not reach the Prowler Cloud API at "
f"{cloud_api_base_url}. Check the URL and your network connection. "
f"Scan results were saved to {ocsf_output.file_path}"
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
except requests.HTTPError as http_err:
logger.warning(
f"OCSF export failed: the API returned HTTP {http_err.response.status_code}. "
"Verify your API key is valid and has the right permissions. "
f"Scan results were saved to {ocsf_output.file_path}"
)
if http_err.response.status_code == 402:
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: "
"this feature is only available with a Prowler Cloud subscription. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
else:
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: the API returned HTTP {http_err.response.status_code}. "
"Verify your API key is valid and has the right permissions. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
except Exception as error:
logger.warning(
f"OCSF export failed unexpectedly: {error}. "
f"Scan results were saved to {ocsf_output.file_path}"
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed unexpectedly: {error}. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
else:
job_id = response.get("data", {}).get("id") if response else None
if job_id:
print(
f"{Style.BRIGHT}{Fore.GREEN}\nOCSF export accepted. Ingestion job: {job_id}{Style.RESET_ALL}"
f"{Style.BRIGHT}{Fore.GREEN}\nFindings successfully pushed to Prowler Cloud. Ingestion job: {job_id}"
f"\nSee more details here: https://cloud.prowler.com/scans{Style.RESET_ALL}"
)
else:
logger.warning(
"OCSF export: unexpected API response (missing ingestion job ID). "
"Push to Prowler Cloud: unexpected API response (missing ingestion job ID). "
f"Scan results were saved to {ocsf_output.file_path}"
)
+19 -19
View File
@@ -74,7 +74,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_invite_only_for_admin_roles",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
@@ -94,7 +94,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_default_users_cannot_create_security_groups",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
@@ -286,7 +286,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_default_users_cannot_create_security_groups",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_user_consent_for_verified_apps",
@@ -709,7 +709,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
@@ -2122,7 +2122,7 @@
"monitor_alert_delete_public_ip_address_rule",
"aks_clusters_public_access_disabled",
"app_function_access_keys_configured",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -3497,7 +3497,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
@@ -4522,7 +4522,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
@@ -4894,7 +4894,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
@@ -4917,7 +4917,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
@@ -5053,7 +5053,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
]
@@ -5298,7 +5298,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5346,7 +5346,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_policy_user_consent_for_verified_apps",
"entra_user_with_vm_access_has_mfa",
@@ -5429,7 +5429,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa"
]
},
@@ -5518,7 +5518,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
]
@@ -5557,7 +5557,7 @@
"app_function_not_publicly_accessible",
"containerregistry_not_publicly_accessible",
"containerregistry_uses_private_link",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5598,7 +5598,7 @@
"app_function_not_publicly_accessible",
"containerregistry_not_publicly_accessible",
"containerregistry_uses_private_link",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -9010,7 +9010,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -9029,7 +9029,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_privileged_user_has_mfa"
]
},
@@ -9240,7 +9240,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_guest_users_access_restrictions",
"iam_custom_role_has_permissions_to_administer_resource_locks",
+10 -10
View File
@@ -1414,7 +1414,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_privileged_user_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5135,7 +5135,7 @@
"Checks": [
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_security_defaults_enabled",
"entra_user_with_vm_access_has_mfa"
]
@@ -5201,7 +5201,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled"
@@ -5266,7 +5266,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled"
@@ -5331,7 +5331,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -5411,7 +5411,7 @@
"keyvault_rbac_enabled",
"keyvault_private_endpoints",
"keyvault_access_only_through_private_endpoints",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_security_defaults_enabled",
@@ -5506,7 +5506,7 @@
"aks_clusters_public_access_disabled",
"app_function_not_publicly_accessible",
"entra_global_admin_in_less_than_five_users",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_trusted_named_locations_exists",
@@ -5571,7 +5571,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5681,7 +5681,7 @@
"network_ssh_internet_access_restricted",
"network_udp_internet_access_restricted",
"vm_jit_access_enabled",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
@@ -5845,7 +5845,7 @@
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"keyvault_rbac_enabled",
"vm_jit_access_enabled",
"vm_linux_enforce_ssh_authentication"
+1 -1
View File
@@ -688,7 +688,7 @@
"Id": "1.2.6",
"Description": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
],
"Attributes": [
{
+1 -1
View File
@@ -729,7 +729,7 @@
"Id": "2.2.7",
"Description": "Ensure Multi-factor Authentication is Required for Windows Azure Service Management API",
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
],
"Attributes": [
{
+1 -1
View File
@@ -1013,7 +1013,7 @@
"Id": "6.2.6",
"Description": "Ensure that multifactor authentication is required for Windows Azure Service Management API",
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
],
"Attributes": [
{
+1 -1
View File
@@ -449,7 +449,7 @@
"Id": "5.2.6",
"Description": "Ensure that multifactor authentication is required for Windows Azure Service Management API",
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
],
"Attributes": [
{
@@ -3703,7 +3703,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -3921,7 +3921,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled",
@@ -279,7 +279,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
]
},
{
@@ -329,7 +329,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
]
},
{
@@ -484,7 +484,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
]
},
{
@@ -89,7 +89,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
+2 -2
View File
@@ -142,7 +142,7 @@
"entra_privileged_user_has_mfa",
"entra_non_privileged_user_has_mfa",
"entra_security_defaults_enabled",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa",
"network_flow_log_captured_sent",
"app_http_logs_enabled"
@@ -730,7 +730,7 @@
"entra_security_defaults_enabled",
"entra_privileged_user_has_mfa",
"entra_non_privileged_user_has_mfa",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa",
"entra_trusted_named_locations_exists",
"sqlserver_azuread_administrator_enabled",
@@ -35,7 +35,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
@@ -307,7 +307,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -212,7 +212,7 @@
"Description": "Adversaries may obtain and abuse credentials of existing accounts as a means of gaining Initial Access, Persistence, Privilege Escalation, or Defense Evasion. Compromised credentials may be used to bypass access controls placed on various resources on systems within the network and may even be used for persistent access to remote systems and externally available services, such as VPNs, Outlook Web Access, network devices, and remote desktop.[1] Compromised credentials may also grant an adversary increased privilege to specific systems or access to restricted areas of the network. Adversaries may choose not to use malware or tools in conjunction with the legitimate access those credentials provide to make it harder to detect their presence.",
"TechniqueURL": "https://attack.mitre.org/techniques/T1078/",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
@@ -489,7 +489,7 @@
"Description": "Adversaries may manipulate accounts to maintain access to victim systems. Account manipulation may consist of any action that preserves adversary access to a compromised account, such as modifying credentials or permission groups. These actions could also include account activity designed to subvert security policies, such as performing iterative password updates to bypass password duration policies and preserve the life of compromised credentials.",
"TechniqueURL": "https://attack.mitre.org/techniques/T1098/",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
@@ -804,7 +804,7 @@
"Description": "Adversaries may modify authentication mechanisms and processes to access user credentials or enable otherwise unwarranted access to accounts. The authentication process is handled by mechanisms, such as the Local Security Authentication Server (LSASS) process and the Security Accounts Manager (SAM) on Windows, pluggable authentication modules (PAM) on Unix-based systems, and authorization plugins on MacOS systems, responsible for gathering, storing, and validating credentials. By modifying an authentication process, an adversary may be able to authenticate to a service or system without using Valid Accounts.",
"TechniqueURL": "https://attack.mitre.org/techniques/T1556/",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
@@ -1279,7 +1279,7 @@
"Description": "Adversaries may sniff network traffic to capture information about an environment, including authentication material passed over the network. Network sniffing refers to using the network interface on a system to monitor or capture information sent over a wired or wireless connection. An adversary may place a network interface into promiscuous mode to passively access data in transit over the network, or use span ports to capture a larger amount of data.",
"TechniqueURL": "https://attack.mitre.org/techniques/T1040/",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
+4 -4
View File
@@ -1603,7 +1603,7 @@
"Id": "11.3.2.a",
"Description": "establish strong identification, authentication such as multi-factor authentication, and authorisation procedures for privileged accounts and system administration accounts;",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -1692,7 +1692,7 @@
"Checks": [
"entra_trusted_named_locations_exists",
"entra_user_with_vm_access_has_mfa",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_privileged_user_has_mfa"
],
"Attributes": [
@@ -1762,7 +1762,7 @@
"Id": "11.6.2.a",
"Description": "ensure the strength of authentication is appropriate to the classification of the asset to be accessed;",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -1794,7 +1794,7 @@
"Id": "11.7.2",
"Description": "The relevant entities shall ensure that the strength of authentication is appropriate for the classification of the asset to be accessed.",
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -45,7 +45,7 @@
"Id": "1.1.3",
"Description": "Ensure Multi-factor Authentication is Required for Windows Azure Service Management API",
"Checks": [
"entra_require_mfa_for_management_api"
"entra_conditional_access_policy_require_mfa_for_management_api"
],
"Attributes": [
{
@@ -160,7 +160,7 @@
"entra_policy_restricts_user_consent_for_apps",
"entra_user_with_vm_access_has_mfa",
"entra_security_defaults_enabled",
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_trusted_named_locations_exists",
"sqlserver_azuread_administrator_enabled",
"postgresql_flexible_server_entra_id_authentication_enabled",
+1 -1
View File
@@ -18,7 +18,7 @@
}
],
"Checks": [
"entra_require_mfa_for_management_api",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
+3 -1
View File
@@ -32,6 +32,7 @@
"Id": "1.1.2",
"Description": "Emergency access or \"break glass\" accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. These emergencies could be due to several things, including:- Technical failures of a cellular provider or Microsoft related service such as MFA.- The last remaining Global Administrator account is inaccessible.Ensure two `Emergency Access` accounts have been defined.**Note:** Microsoft provides several recommendations for these accounts and how to configure them. For more information on this, please refer to the references section. The CIS Benchmark outlines the more critical things to consider.",
"Checks": [
"entra_break_glass_account_fido2_security_key_registered",
"entra_emergency_access_exclusion"
],
"Attributes": [
@@ -1214,7 +1215,8 @@
"Id": "5.2.2.5",
"Description": "Authentication strength is a Conditional Access control that allows administrators to specify which combination of authentication methods can be used to access a resource. For example, they can make only phishing-resistant authentication methods available to access a sensitive resource. But to access a non-sensitive resource, they can allow less secure multifactor authentication (MFA) combinations, such as password + SMS.Microsoft has 3 built-in authentication strengths. MFA strength, Passwordless MFA strength, and Phishing-resistant MFA strength. Ensure administrator roles are using a CA policy with `Phishing-resistant MFA strength`.Administrators can then enroll using one of 3 methods:- FIDO2 Security Key- Windows Hello for Business- Certificate-based authentication (Multi-Factor)**Note:** Additional steps to configure methods such as FIDO2 keys are not covered here but can be found in related MS articles in the references section. The Conditional Access policy only ensures 1 of the 3 methods is used.**Warning:** Administrators should be pre-registered for a strong authentication mechanism before this Conditional Access Policy is enforced. Additionally, as stated elsewhere in the CIS Benchmark a break-glass administrator account should be excluded from this policy to ensure unfettered access in the case of an emergency.",
"Checks": [
"entra_admin_users_phishing_resistant_mfa_enabled"
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_break_glass_account_fido2_security_key_registered"
],
"Attributes": [
{
+3 -1
View File
@@ -32,6 +32,7 @@
"Id": "1.1.2",
"Description": "Emergency access or 'break glass' accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. Ensure two Emergency Access accounts have been defined.",
"Checks": [
"entra_break_glass_account_fido2_security_key_registered",
"entra_emergency_access_exclusion"
],
"Attributes": [
@@ -1446,7 +1447,8 @@
"Id": "5.2.2.5",
"Description": "Authentication strength is a Conditional Access control that allows administrators to specify which combination of authentication methods can be used to access a resource. Ensure administrator roles are using a CA policy with Phishing-resistant MFA strength.",
"Checks": [
"entra_admin_users_phishing_resistant_mfa_enabled"
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_break_glass_account_fido2_security_key_registered"
],
"Attributes": [
{
@@ -174,7 +174,7 @@
}
],
"Checks": [
"entra_app_enforced_restrictions",
"entra_conditional_access_policy_app_enforced_restrictions",
"exchange_transport_config_smtp_auth_disabled",
"exchange_transport_rules_mail_forwarding_disabled",
"exchange_transport_rules_whitelist_disabled",
@@ -240,6 +240,7 @@
"defenderxdr_endpoint_privileged_user_exposed_credentials",
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_break_glass_account_fido2_security_key_registered",
"entra_default_app_management_policy_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_legacy_authentication_blocked",
@@ -615,7 +616,7 @@
"Checks": [
"defenderxdr_endpoint_privileged_user_exposed_credentials",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_app_enforced_restrictions",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_managed_device_required_for_authentication",
"entra_managed_device_required_for_mfa_registration",
"entra_users_mfa_capable",
@@ -649,6 +650,7 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_break_glass_account_fido2_security_key_registered",
"entra_app_registration_no_unused_privileged_permissions",
"entra_policy_ensure_default_user_cannot_create_tenants",
"entra_policy_guest_invite_only_for_admin_roles",
@@ -669,7 +671,7 @@
],
"Checks": [
"entra_admin_portals_access_restriction",
"entra_app_enforced_restrictions",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_policy_guest_users_access_restrictions",
"sharepoint_external_sharing_restricted"
]
@@ -690,6 +692,7 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_break_glass_account_fido2_security_key_registered",
"entra_identity_protection_sign_in_risk_enabled",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -755,7 +758,7 @@
"defender_antiphishing_policy_configured",
"defender_safelinks_policy_enabled",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_app_enforced_restrictions"
"entra_conditional_access_policy_app_enforced_restrictions"
]
},
{
@@ -27,7 +27,8 @@
"Id": "1.1.2",
"Description": "Ensure multifactor authentication is enabled for all users in administrative roles",
"Checks": [
"entra_admin_users_mfa_enabled"
"entra_admin_users_mfa_enabled",
"entra_break_glass_account_fido2_security_key_registered"
],
"Attributes": [
{
@@ -81,7 +82,8 @@
"Id": "1.1.5",
"Description": "Ensure 'Phishing-resistant MFA strength' is required for Administrators",
"Checks": [
"entra_admin_users_phishing_resistant_mfa_enabled"
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_break_glass_account_fido2_security_key_registered"
],
"Attributes": [
{
@@ -823,7 +825,7 @@
"Id": "1.3.9",
"Description": "Ensure OneDrive sync is restricted for unmanaged devices",
"Checks": [
"entra_app_enforced_restrictions",
"entra_conditional_access_policy_app_enforced_restrictions",
"sharepoint_onedrive_sync_restricted_unmanaged_devices"
],
"Attributes": [
+3 -3
View File
@@ -38,7 +38,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.19.0"
prowler_version = "5.19.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"
@@ -122,8 +122,8 @@ encoding_format_utf_8 = "utf-8"
available_output_formats = ["csv", "json-asff", "json-ocsf", "html"]
# Prowler Cloud API settings
cloud_api_base_url = os.getenv("PROWLER_CLOUD_API_BASE", "https://api.prowler.com")
cloud_api_key = os.getenv("PROWLER_API_KEY", "")
cloud_api_base_url = os.getenv("PROWLER_CLOUD_API_BASE_URL", "https://api.prowler.com")
cloud_api_key = os.getenv("PROWLER_CLOUD_API_KEY", "")
cloud_api_ingestion_path = "/api/v1/ingestions"
+4 -1
View File
@@ -18,7 +18,10 @@ def print_banner(legend: bool = False):
| '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|
| |_) | | | (_) \ V V /| | __/ |
| .__/|_| \___/ \_/\_/ |_|\___|_|v{prowler_version}
|_|{Fore.BLUE} the handy multi-cloud security tool
|_|{Fore.BLUE} Get the most at https://cloud.prowler.com {Style.RESET_ALL}
{Fore.GREEN}New! Send findings from Prowler CLI to Prowler Cloud{Style.RESET_ALL}
{Fore.GREEN}More details here: goto.prowler.com/import-findings{Style.RESET_ALL}
{Fore.YELLOW}Date: {timestamp.strftime("%Y-%m-%d %H:%M:%S")}{Style.RESET_ALL}
"""
+2 -2
View File
@@ -818,11 +818,11 @@ class CheckReportCloudflare(Check_Report):
@property
def account_id(self) -> str:
"""Account ID derived from zone's account."""
"""Account ID derived from resource's account object or flat account_id."""
zone_account = getattr(self._zone, "account", None)
if zone_account:
return getattr(zone_account, "id", "")
return ""
return getattr(self._zone, "account_id", "")
@property
def region(self) -> str:
+5 -3
View File
@@ -217,11 +217,13 @@ Detailed documentation at https://docs.prowler.com
help="Set the output timestamp format as unix timestamps instead of iso format timestamps (default mode).",
)
common_outputs_parser.add_argument(
"--export-ocsf",
"--push-to-cloud",
action="store_true",
help=(
"Send OCSF output to Prowler Cloud ingestion endpoint. "
"Requires PROWLER_API_KEY environment variable."
"Send findings in OCSF format to Prowler Cloud. "
"Requires PROWLER_CLOUD_API_KEY environment variable. "
"For the IaC provider, --provider-uid is also required. "
"More details here: https://goto.prowler.com/import-findings"
),
)
+44 -9
View File
@@ -34,6 +34,7 @@ class Finding(BaseModel):
auth_method: str
timestamp: Union[int, datetime]
account_uid: str
provider_uid: Optional[str] = None
account_name: Optional[str] = None
account_email: Optional[str] = None
account_organization_uid: Optional[str] = None
@@ -244,6 +245,7 @@ class Finding(BaseModel):
output_data["account_uid"] = get_nested_attribute(
provider, "identity.cluster"
)
output_data["provider_uid"] = provider.identity.context
output_data["region"] = f"namespace: {check_output.namespace}"
elif provider.type == "github":
@@ -251,15 +253,22 @@ class Finding(BaseModel):
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
owner = getattr(check_output, "owner", None)
if isinstance(provider.identity, GithubIdentityInfo):
# GithubIdentityInfo (Personal Access Token, OAuth)
output_data["account_name"] = provider.identity.account_name
output_data["account_uid"] = provider.identity.account_id
output_data["account_name"] = (
owner or provider.identity.account_name
)
output_data["account_uid"] = owner or provider.identity.account_name
output_data["account_email"] = provider.identity.account_email
elif isinstance(provider.identity, GithubAppIdentityInfo):
# GithubAppIdentityInfo (GitHub App)
output_data["account_name"] = provider.identity.app_name
output_data["account_uid"] = provider.identity.app_id
output_data["account_name"] = owner or provider.identity.app_name
output_data["account_uid"] = owner or provider.identity.app_name
output_data["account_organization_uid"] = str(
provider.identity.app_id
)
output_data["installations"] = provider.identity.installations
output_data["region"] = check_output.owner
@@ -269,11 +278,14 @@ class Finding(BaseModel):
f"{provider.identity.identity_type}: {provider.identity.identity_id}"
)
output_data["account_uid"] = get_nested_attribute(
provider, "identity.tenant_id"
provider, "identity.tenant_domain"
)
output_data["account_name"] = get_nested_attribute(
provider, "identity.tenant_domain"
)
output_data["account_organization_uid"] = get_nested_attribute(
provider, "identity.tenant_id"
)
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = check_output.location
@@ -321,8 +333,9 @@ class Finding(BaseModel):
elif provider.type == "iac":
output_data["auth_method"] = provider.auth_method
output_data["account_uid"] = "iac"
output_data["account_name"] = "iac"
provider_uid = getattr(provider, "provider_uid", None)
output_data["account_uid"] = provider_uid if provider_uid else "iac"
output_data["account_name"] = provider_uid if provider_uid else "iac"
output_data["resource_name"] = getattr(
check_output, "resource_name", ""
)
@@ -358,8 +371,27 @@ class Finding(BaseModel):
elif provider.type == "cloudflare":
output_data["auth_method"] = "api_token"
output_data["account_uid"] = check_output.account_id
output_data["account_name"] = check_output.account_id
account_id = check_output.account_id
if not account_id:
audited_accounts = (
get_nested_attribute(provider, "identity.audited_accounts")
or []
)
if audited_accounts:
account_id = audited_accounts[0]
account_name = account_id
if account_id:
accounts = get_nested_attribute(provider, "identity.accounts") or []
for account in accounts:
if getattr(account, "id", None) == account_id and getattr(
account, "name", None
):
account_name = account.name
break
output_data["account_uid"] = account_id or ""
output_data["account_name"] = account_name or account_id or ""
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = check_output.zone_name
@@ -421,6 +453,9 @@ class Finding(BaseModel):
f"{output_data['region']}-{output_data['resource_name']}"
)
if provider.type == "iac" and output_data.get("resource_line_range"):
output_data["uid"] += f"-{output_data['resource_line_range']}"
if not output_data["resource_uid"]:
logger.error(
f"Check {check_output.check_metadata.CheckID} has no resource_uid."
+5 -3
View File
@@ -21,9 +21,9 @@ def send_ocsf_to_api(
Args:
file_path: Path to the OCSF JSON file to upload.
base_url: API base URL. Falls back to PROWLER_CLOUD_API_BASE env var,
base_url: API base URL. Falls back to PROWLER_CLOUD_API_BASE_URL env var,
then to https://api.prowler.com.
api_key: API key. Falls back to PROWLER_API_KEY env var.
api_key: API key. Falls back to PROWLER_CLOUD_API_KEY env var.
timeout: Request timeout in seconds.
Returns:
@@ -42,7 +42,9 @@ def send_ocsf_to_api(
api_key = api_key or cloud_api_key
if not api_key:
raise ValueError("Missing API key. Set PROWLER_API_KEY environment variable.")
raise ValueError(
"Missing API key. Set PROWLER_CLOUD_API_KEY environment variable."
)
base_url = base_url or cloud_api_base_url
base_url = base_url.rstrip("/")
+2
View File
@@ -178,6 +178,8 @@ class OCSF(Output):
"notes": finding.metadata.Notes,
"compliance": finding.compliance,
"scan_id": str(scan_id),
"provider_uid": finding.provider_uid or finding.account_uid,
"provider": finding.provider,
},
)
if finding.provider != "kubernetes":
+6 -1
View File
@@ -302,7 +302,12 @@ class Scan:
for report in iac_reports:
# Generate unique UID for the finding
finding_uid = f"{report.check_metadata.CheckID}-{report.resource_name}-{report.resource_line_range}"
finding_uid = (
f"prowler-iac-{report.check_metadata.CheckID}-iac-"
f"{report.region}-{report.resource_name}"
)
if report.resource_line_range:
finding_uid += f"-{report.resource_line_range}"
# Convert status string to Status enum
status_enum = (
+3 -1
View File
@@ -1475,7 +1475,9 @@ class AwsProvider(Provider):
sts_client = create_sts_session(session, 'us-west-2')
"""
try:
if aws_region.startswith("cn-"):
if os.environ.get("AWS_ENDPOINT_URL"):
sts_endpoint_url = os.environ["AWS_ENDPOINT_URL"]
elif aws_region.startswith("cn-"):
sts_endpoint_url = f"https://sts.{aws_region}.amazonaws.com.cn"
elif aws_region.startswith("eusc-"):
sts_endpoint_url = f"https://sts.{aws_region}.amazonaws.eu"
@@ -80,7 +80,7 @@ def init_parser(self):
"--security-hub",
"-S",
action="store_true",
help="Send check output to AWS Security Hub and save json-asff outuput.",
help="Send check output to AWS Security Hub and save json-asff output.",
)
aws_security_hub_subparser.add_argument(
"--skip-sh-update",
@@ -16,6 +16,19 @@ class elbv2_insecure_ssl_ciphers(Check):
"ELBSecurityPolicy-TLS13-1-2-Res-2021-06",
"ELBSecurityPolicy-TLS13-1-2-Ext1-2021-06",
"ELBSecurityPolicy-TLS13-1-2-Ext2-2021-06",
# AWS post-quantum (PQ) TLS policies (TLS 1.2+ minimum)
"ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-3-PQ-2025-09",
# AWS FIPS post-quantum (PQ) TLS policies (TLS 1.2+ minimum)
"ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09",
"ELBSecurityPolicy-TLS13-1-3-FIPS-PQ-2025-09",
]
for lb in elbv2_client.loadbalancersv2.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=lb)
@@ -21,7 +21,7 @@ class DNS(CloudflareService):
# Get zones directly from API to avoid circular dependency with zone_client
zones = self._get_zones()
for zone_id, zone_name in zones.items():
for zone_id, (zone_name, account_id) in zones.items():
seen_record_ids: set[str] = set()
try:
for record in self.client.dns.records.list(zone_id=zone_id):
@@ -36,6 +36,7 @@ class DNS(CloudflareService):
id=record_id,
zone_id=zone_id,
zone_name=zone_name,
account_id=account_id,
name=getattr(record, "name", None),
type=getattr(record, "type", None),
content=getattr(record, "content", ""),
@@ -52,11 +53,11 @@ class DNS(CloudflareService):
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_zones(self) -> dict[str, str]:
def _get_zones(self) -> dict[str, tuple[str, str]]:
"""Get zones directly from Cloudflare API.
Returns:
Dictionary mapping zone_id to zone_name.
Dictionary mapping zone_id to (zone_name, account_id).
"""
zones = {}
audited_accounts = self.provider.identity.audited_accounts
@@ -88,7 +89,7 @@ class DNS(CloudflareService):
):
continue
zones[zone_id] = zone_name
zones[zone_id] = (zone_name, account_id or "")
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -103,6 +104,7 @@ class CloudflareDNSRecord(BaseModel):
id: str
zone_id: str
zone_name: str
account_id: str = ""
name: Optional[str] = None
type: Optional[str] = None
content: str = ""
+1
View File
@@ -273,6 +273,7 @@ class Provider(ABC):
github_username=arguments.github_username,
personal_access_token=arguments.personal_access_token,
oauth_app_token=arguments.oauth_app_token,
provider_uid=arguments.provider_uid,
)
elif "llm" in provider_class_name.lower():
provider_class(
+6
View File
@@ -38,6 +38,7 @@ class IacProvider(Provider):
github_username: str = None,
personal_access_token: str = None,
oauth_app_token: str = None,
provider_uid: str = None,
):
logger.info("Instantiating IAC Provider...")
@@ -47,6 +48,7 @@ class IacProvider(Provider):
self.exclude_path = exclude_path
self.region = "branch"
self.audited_account = "local-iac"
self._provider_uid = provider_uid
self._session = None
self._identity = "prowler"
self._auth_method = "No auth"
@@ -146,6 +148,10 @@ class IacProvider(Provider):
def fixer_config(self):
return self._fixer_config
@property
def provider_uid(self):
return self._provider_uid
def __del__(self):
"""Cleanup temporary directory when provider is destroyed"""
self.cleanup()
@@ -1,3 +1,5 @@
import re
SCANNERS_CHOICES = [
"vuln",
"misconfig",
@@ -37,14 +39,14 @@ def init_parser(self):
nargs="+",
default=["misconfig", "secret"],
choices=SCANNERS_CHOICES,
help="Comma-separated list of scanners to scan. Default: misconfig, secret",
help="Space-separated list of scanners to scan. Default: misconfig secret",
)
iac_scan_subparser.add_argument(
"--exclude-path",
dest="exclude_path",
nargs="+",
default=[],
help="Comma-separated list of paths to exclude from the scan. Default: none",
help="Space-separated list of paths to exclude from the scan. Default: none",
)
iac_scan_subparser.add_argument(
@@ -68,6 +70,12 @@ def init_parser(self):
default=None,
help="GitHub OAuth app token for authenticated repository cloning. If not provided, will use GITHUB_OAUTH_APP_TOKEN env var.",
)
iac_scan_subparser.add_argument(
"--provider-uid",
dest="provider_uid",
default=None,
help="Unique identifier for the IaC provider. Required when using --push-to-cloud.",
)
def validate_arguments(arguments):
@@ -80,4 +88,19 @@ def validate_arguments(arguments):
False,
"--scan-path (-P) and --scan-repository-url (-R) are mutually exclusive. Please specify only one.",
)
push_to_cloud = getattr(arguments, "push_to_cloud", False)
provider_uid = getattr(arguments, "provider_uid", None)
if push_to_cloud and not provider_uid:
return (
False,
"--provider-uid is required when using --push-to-cloud with the IAC provider.",
)
if provider_uid and not re.match(
r"^(https?://|git@|ssh://)[^\s/]+[^\s]*\.git$|^(https?://)[^\s/]+[^\s]*$",
provider_uid,
):
return (
False,
"--provider-uid must be a valid repository URL (e.g., https://github.com/user/repo or https://github.com/user/repo.git).",
)
return (True, "")
@@ -62,7 +62,7 @@ def init_parser(self):
default="M365Global",
choices=[
"M365Global",
"M365GlobalChina",
"M365China",
"M365USGovernment",
],
help="Microsoft 365 region to be used, default is M365Global",
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_authentication_method_sms_voice_disabled",
"CheckTitle": "SMS and Voice authentication methods are disabled in the tenant",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "SMS and Voice authentication methods should be disabled in the tenant's authentication methods policy. These methods are vulnerable to **SIM-swapping**, **interception**, and **social engineering** attacks, and are deprecated by NIST SP 800-63B as out-of-band authenticators.",
"Risk": "Enabled SMS or Voice authentication allows attackers to bypass MFA through **SIM-swapping** or **SS7 protocol interception**, gaining unauthorized access to accounts. These methods lack cryptographic binding to the device, making them significantly weaker than phishing-resistant alternatives.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-phone-options",
"https://pages.nist.gov/800-63-3/sp800-63b.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Policies**\n3. Select **SMS** and set its status to **Disabled**, then click **Save**\n4. Select **Voice call** and set its status to **Disabled**, then click **Save**\n5. Ensure users have alternative phishing-resistant MFA methods configured (e.g., FIDO2, Microsoft Authenticator)",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable SMS and Voice authentication methods and adopt **phishing-resistant** alternatives such as FIDO2 security keys or Microsoft Authenticator. Use Authentication Strengths in Conditional Access policies to enforce only strong MFA methods across the tenant.",
"Url": "https://hub.prowler.com/check/entra_authentication_method_sms_voice_disabled"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,69 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
class entra_authentication_method_sms_voice_disabled(Check):
"""
Ensure that SMS and Voice authentication methods are disabled in Microsoft Entra.
This check verifies that the tenant's authentication methods policy has both SMS and
Voice methods disabled, as they are vulnerable to SIM-swapping, interception, and
social engineering attacks. NIST SP 800-63B deprecates SMS as an out-of-band
authenticator.
- PASS: Both SMS and Voice authentication methods are disabled.
- FAIL: SMS and/or Voice authentication methods are enabled.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the SMS and Voice authentication method check.
Evaluates the authentication method configurations from the Entra client
and checks whether both SMS and Voice methods are disabled.
Returns:
A list with a single report containing the result of the check.
"""
findings = []
configs = entra_client.authentication_method_configurations
sms_config = configs.get("Sms")
voice_config = configs.get("Voice")
if sms_config or voice_config:
sms_enabled = sms_config and sms_config.state == "enabled"
voice_enabled = voice_config and voice_config.state == "enabled"
report = CheckReportM365(
metadata=self.metadata(),
resource=sms_config or voice_config,
resource_name="SMS and Voice Authentication Methods",
resource_id=entra_client.tenant_domain,
)
if sms_enabled and voice_enabled:
report.status = "FAIL"
report.status_extended = (
"SMS and Voice authentication methods are enabled in the tenant."
)
elif sms_enabled:
report.status = "FAIL"
report.status_extended = (
"SMS authentication method is enabled in the tenant."
)
elif voice_enabled:
report.status = "FAIL"
report.status_extended = (
"Voice authentication method is enabled in the tenant."
)
else:
report.status = "PASS"
report.status_extended = (
"SMS and Voice authentication methods are disabled in the tenant."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "m365",
"CheckID": "entra_break_glass_account_fido2_security_key_registered",
"CheckTitle": "Break glass account has a FIDO2 security key registered for phishing-resistant authentication",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Break glass (emergency access) accounts should have at least one **FIDO2 security key** registered as their authentication method. These accounts are identified as users excluded from all enabled Conditional Access policies.",
"Risk": "Without FIDO2 security keys, break glass accounts rely on weaker authentication methods vulnerable to **phishing, credential theft, and man-in-the-middle attacks**. Compromised emergency access accounts could grant an attacker unrestricted tenant access, bypassing all Conditional Access protections.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access",
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless#fido2-security-keys"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Entra admin center > Users.\n2. Select the break glass account.\n3. Go to Authentication methods > Add authentication method.\n4. Select FIDO2 Security Key and follow the registration steps.\n5. Store the physical FIDO2 key in a secure location (e.g., physical safe).",
"Terraform": ""
},
"Recommendation": {
"Text": "Register at least one **FIDO2 security key** for each break glass account. Store the physical keys in a secure, offline location such as a safe. Use phishing-resistant authentication to protect emergency access accounts from credential-based attacks.",
"Url": "https://hub.prowler.com/check/entra_break_glass_account_fido2_security_key_registered"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [
"entra_emergency_access_exclusion"
],
"Notes": "Break glass accounts are identified as users excluded from all enabled Conditional Access policies. This check requires the entra_emergency_access_exclusion check to pass first for meaningful results."
}
@@ -0,0 +1,104 @@
from collections import Counter
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicyState,
)
class entra_break_glass_account_fido2_security_key_registered(Check):
"""Ensure that break glass accounts have FIDO2 security keys registered.
This check identifies break glass (emergency access) accounts by finding users
excluded from all enabled Conditional Access policies, then verifies each has
at least one FIDO2 security key registered as an authentication method.
- PASS: The break glass account has a FIDO2 security key (fido2SecurityKey) registered.
- MANUAL: The account has a device-bound passkey but it cannot be confirmed as FIDO2,
or no break glass accounts could be identified.
- FAIL: The break glass account does not have a FIDO2 security key registered.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check for FIDO2 registration on break glass accounts.
Returns:
A list of reports containing the result of the check.
"""
findings = []
enabled_policies = [
policy
for policy in entra_client.conditional_access_policies.values()
if policy.state != ConditionalAccessPolicyState.DISABLED
]
if not enabled_policies:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Break Glass Accounts",
resource_id="breakGlassAccounts",
)
report.status = "MANUAL"
report.status_extended = "No enabled Conditional Access policies found. Break glass accounts cannot be identified to verify FIDO2 registration."
findings.append(report)
return findings
total_policy_count = len(enabled_policies)
excluded_users_counter = Counter()
for policy in enabled_policies:
user_conditions = policy.conditions.user_conditions
if user_conditions:
for user_id in user_conditions.excluded_users:
excluded_users_counter[user_id] += 1
break_glass_user_ids = [
user_id
for user_id, count in excluded_users_counter.items()
if count == total_policy_count
]
if not break_glass_user_ids:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Break Glass Accounts",
resource_id="breakGlassAccounts",
)
report.status = "MANUAL"
report.status_extended = "No break glass accounts identified. No users are excluded from all enabled Conditional Access policies."
findings.append(report)
return findings
for user_id in break_glass_user_ids:
user = entra_client.users.get(user_id)
if not user:
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.id,
)
auth_methods = set(user.authentication_methods)
has_fido2 = "fido2SecurityKey" in auth_methods
has_passkey_device_bound = "passKeyDeviceBound" in auth_methods
if has_fido2:
report.status = "PASS"
report.status_extended = f"Break glass account {user.name} has a FIDO2 security key registered."
elif has_passkey_device_bound:
report.status = "MANUAL"
report.status_extended = f"Break glass account {user.name} has a device-bound passkey registered, but it cannot be confirmed whether it is a FIDO2 security key."
else:
report.status = "FAIL"
report.status_extended = f"Break glass account {user.name} does not have a FIDO2 security key registered."
findings.append(report)
return findings
@@ -1,6 +1,6 @@
{
"Provider": "m365",
"CheckID": "entra_app_enforced_restrictions",
"CheckID": "entra_conditional_access_policy_app_enforced_restrictions",
"CheckTitle": "Conditional Access policy enforces application restrictions for unmanaged devices",
"CheckType": [],
"ServiceName": "entra",
@@ -25,7 +25,7 @@
},
"Recommendation": {
"Text": "Configure Conditional Access policies with **application enforced restrictions** to control access from unmanaged devices. Apply this to Office 365 applications (SharePoint, OneDrive, Exchange) to limit download, print, and sync operations.\n\nCombine with SharePoint access control settings for comprehensive protection.",
"Url": "https://hub.prowler.com/check/entra_app_enforced_restrictions"
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_app_enforced_restrictions"
}
},
"Categories": [
@@ -6,7 +6,7 @@ from prowler.providers.m365.services.entra.entra_service import (
)
class entra_app_enforced_restrictions(Check):
class entra_conditional_access_policy_app_enforced_restrictions(Check):
"""Check if at least one Conditional Access policy enforces application restrictions.
This check verifies that the tenant has at least one enabled Conditional Access policy
@@ -1,6 +1,6 @@
{
"Provider": "m365",
"CheckID": "entra_require_mfa_for_management_api",
"CheckID": "entra_conditional_access_policy_require_mfa_for_management_api",
"CheckTitle": "Conditional Access Policy enforces MFA for Azure Management API access",
"CheckType": [],
"ServiceName": "entra",
@@ -25,7 +25,7 @@
},
"Recommendation": {
"Text": "Enforce **MFA** via Conditional Access for the Windows Azure Service Management API scoped to all users. Prefer **phishing-resistant** methods, apply **least privilege**, and monitor sign-ins for anomalous activity. Only exclude dedicated break-glass accounts.",
"Url": "https://hub.prowler.com/check/entra_require_mfa_for_management_api"
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_mfa_for_management_api"
}
},
"Categories": [
@@ -9,7 +9,7 @@ from prowler.providers.m365.services.entra.entra_service import (
AZURE_MANAGEMENT_API_APP_ID = "797f4846-ba00-4fd7-ba43-dac1f8f63013"
class entra_require_mfa_for_management_api(Check):
class entra_conditional_access_policy_require_mfa_for_management_api(Check):
"""Check if at least one enabled Conditional Access policy requires MFA for Azure Management API.
This check verifies that at least one enabled Conditional Access policy
@@ -95,10 +95,19 @@ class entra_emergency_access_exclusion(Check):
report.status = "PASS"
exclusion_details = []
if users_excluded_from_all:
exclusion_details.append(f"{len(users_excluded_from_all)} user(s)")
user_names = []
for user_id in users_excluded_from_all:
user = entra_client.users.get(user_id)
user_names.append(user.name if user else user_id)
exclusion_details.append(f"user(s): {', '.join(user_names)}")
if groups_excluded_from_all:
exclusion_details.append(f"{len(groups_excluded_from_all)} group(s)")
report.status_extended = f"{' and '.join(exclusion_details)} excluded as emergency access across all {total_policy_count} enabled Conditional Access policies."
group_names = []
groups_by_id = {g.id: g for g in entra_client.groups}
for group_id in groups_excluded_from_all:
group = groups_by_id.get(group_id)
group_names.append(group.name if group else group_id)
exclusion_details.append(f"group(s): {', '.join(group_names)}")
report.status_extended = f"Emergency access {' and '.join(exclusion_details)} excluded from all {total_policy_count} enabled Conditional Access policies."
else:
report.status = "FAIL"
report.status_extended = f"No user or group is excluded as emergency access from all {total_policy_count} enabled Conditional Access policies."
@@ -35,6 +35,7 @@ class Entra(M365Service):
users (dict): Dictionary of users.
user_accounts_status (dict): Dictionary of user account statuses.
oauth_apps (dict): Dictionary of OAuth applications from Defender XDR.
authentication_method_configurations (dict): Dictionary of authentication method configurations.
"""
def __init__(self, provider: M365Provider):
@@ -81,6 +82,7 @@ class Entra(M365Service):
self._get_default_app_management_policy(),
self._get_oauth_apps(),
self._get_directory_sync_settings(),
self._get_authentication_method_configurations(),
)
)
@@ -93,6 +95,9 @@ class Entra(M365Service):
self.default_app_management_policy = attributes[6]
self.oauth_apps: Optional[Dict[str, OAuthApp]] = attributes[7]
self.directory_sync_settings, self.directory_sync_error = attributes[8]
self.authentication_method_configurations: Dict[
str, AuthenticationMethodConfiguration
] = attributes[9]
self.user_accounts_status = {}
if created_loop:
@@ -585,6 +590,7 @@ class Entra(M365Service):
while users_response:
for user in getattr(users_response, "value", []) or []:
reg_info = registration_details.get(user.id, {})
users[user.id] = User(
id=user.id,
name=user.display_name,
@@ -592,10 +598,13 @@ class Entra(M365Service):
True if (user.on_premises_sync_enabled) else False
),
directory_roles_ids=user_roles_map.get(user.id, []),
is_mfa_capable=(registration_details.get(user.id, False)),
is_mfa_capable=reg_info.get("is_mfa_capable", False),
account_enabled=not self.user_accounts_status.get(
user.id, {}
).get("AccountDisabled", False),
authentication_methods=reg_info.get(
"authentication_methods", []
),
)
next_link = getattr(users_response, "odata_next_link", None)
@@ -609,6 +618,16 @@ class Entra(M365Service):
return users
async def _get_user_registration_details(self):
"""Retrieve user authentication method registration details.
Fetches registration details from the Microsoft Graph API, including
MFA capability and the specific authentication methods each user has registered.
Returns:
dict: A dictionary mapping user IDs to their registration details,
where each value is a dict with 'is_mfa_capable' (bool) and
'authentication_methods' (list of str).
"""
registration_details = {}
try:
registration_builder = (
@@ -618,9 +637,14 @@ class Entra(M365Service):
while registration_response:
for detail in getattr(registration_response, "value", []) or []:
registration_details.update(
{detail.id: getattr(detail, "is_mfa_capable", False)}
)
registration_details[detail.id] = {
"is_mfa_capable": getattr(detail, "is_mfa_capable", False),
"authentication_methods": [
str(method)
for method in getattr(detail, "methods_registered", [])
or []
],
}
next_link = getattr(registration_response, "odata_next_link", None)
if not next_link:
@@ -756,6 +780,41 @@ OAuthAppInfo
return oauth_apps
async def _get_authentication_method_configurations(self):
"""Retrieve authentication method configurations from Microsoft Entra.
Fetches the authentication methods policy and extracts the configuration
state for each authentication method (e.g., SMS, Voice, FIDO2, etc.).
Returns:
Dict[str, AuthenticationMethodConfiguration]: Dictionary of authentication
method configurations keyed by method ID (e.g., 'sms', 'voice').
"""
logger.info("Entra - Getting authentication method configurations...")
authentication_method_configurations = {}
try:
policy = await self.client.policies.authentication_methods_policy.get()
for config in (
getattr(policy, "authentication_method_configurations", []) or []
):
method_id = getattr(config, "id", "")
if method_id:
authentication_method_configurations[method_id] = (
AuthenticationMethodConfiguration(
id=method_id,
state=(
getattr(config, "state", None).value
if getattr(config, "state", None)
else "disabled"
),
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return authentication_method_configurations
class ConditionalAccessPolicyState(Enum):
ENABLED = "enabled"
@@ -916,6 +975,21 @@ class DirectorySyncSettings(BaseModel):
seamless_sso_enabled: bool = False
class AuthenticationMethodConfiguration(BaseModel):
"""Authentication method configuration from the authentication methods policy.
Represents the state of a specific authentication method (e.g., SMS, Voice,
FIDO2) within the tenant's authentication methods policy.
Attributes:
id: The authentication method identifier (e.g., 'sms', 'voice').
state: The state of the authentication method ('enabled' or 'disabled').
"""
id: str
state: str = "disabled"
class Group(BaseModel):
id: str
name: str
@@ -975,12 +1049,26 @@ class AdminRoles(Enum):
class User(BaseModel):
"""Model representing a Microsoft Entra ID user.
Attributes:
id: The user's unique identifier.
name: The user's display name.
on_premises_sync_enabled: Whether the user is synced from on-premises directory.
directory_roles_ids: List of directory role template IDs assigned to the user.
is_mfa_capable: Whether the user has registered a strong authentication method for MFA.
account_enabled: Whether the user account is enabled.
authentication_methods: List of authentication method types registered by the user
(e.g., 'fido2SecurityKey', 'microsoftAuthenticatorPush', 'mobilePhone').
"""
id: str
name: str
on_premises_sync_enabled: bool
directory_roles_ids: List[str] = []
is_mfa_capable: bool = False
account_enabled: bool = True
authentication_methods: List[str] = []
class InvitationsFrom(Enum):
@@ -0,0 +1,131 @@
"""Helper utilities for OpenStack security group checks."""
from ipaddress import IPv4Network, IPv6Network, ip_network
from typing import List, Optional
from prowler.providers.openstack.services.networking.networking_service import (
SecurityGroupRule,
)
def check_security_group_rule(
rule: SecurityGroupRule,
protocol: Optional[str] = None,
ports: Optional[List[int]] = None,
any_address: bool = False,
direction: str = "ingress",
) -> bool:
"""
Check if a security group rule matches specified criteria.
Args:
rule: SecurityGroupRule to check
protocol: Protocol to match (tcp/udp/icmp/None for any)
ports: List of ports to check
any_address: If True, only match 0.0.0.0/0 or ::/0. If False, match public IPs # noqa: E501
direction: Direction to check (ingress/egress)
Returns:
True if rule matches all criteria, False otherwise
"""
# Check direction
if rule.direction != direction:
return False
# Check protocol
if protocol is not None:
# None protocol means all protocols in OpenStack
if rule.protocol is not None and rule.protocol != protocol:
return False
# Check ports
if ports is not None and len(ports) > 0:
# If rule has no port range, it allows all ports (protocol-level rule)
if rule.port_range_min is None and rule.port_range_max is None:
# No port range means all ports for the protocol (or all
# protocols if protocol is also None). This always matches.
pass
else:
# Check if any of the target ports fall within the rule's range
port_matches = False
for port in ports:
if is_port_in_range(
port, rule.port_range_min, rule.port_range_max
): # noqa: E501
port_matches = True
break
if not port_matches:
return False
# Check CIDR - must be publicly accessible
if rule.remote_ip_prefix:
if not is_cidr_public(rule.remote_ip_prefix, any_address=any_address):
return False
elif rule.remote_group_id:
# Remote group rules are not public
return False
else:
# No IP prefix or group means all IPs (0.0.0.0/0)
pass
return True
def is_port_in_range(
port: int, range_min: Optional[int], range_max: Optional[int]
) -> bool:
"""
Check if a port falls within the specified range.
Args:
port: Port number to check
range_min: Minimum port in range (None means no minimum)
range_max: Maximum port in range (None means no maximum)
Returns:
True if port is in range, False otherwise
"""
if range_min is None and range_max is None:
return True
if range_min is None:
return port <= range_max
if range_max is None:
return port >= range_min
return range_min <= port <= range_max
def is_cidr_public(cidr: str, any_address: bool = False) -> bool:
"""
Check if a CIDR block represents public/internet access.
Args:
cidr: CIDR block to check (e.g., "0.0.0.0/0", "10.0.0.0/8")
any_address: If True, only match 0.0.0.0/0 or ::/0.
If False, match any globally routable IP.
Returns:
True if CIDR represents public access, False otherwise
"""
if not cidr:
return False
try:
network = ip_network(cidr, strict=False)
if any_address:
# Only match 0.0.0.0/0 or ::/0
if isinstance(network, IPv4Network):
return str(network) == "0.0.0.0/0"
elif isinstance(network, IPv6Network):
return str(network) == "::/0"
return False
else:
# Match any globally routable (public) IP
# is_global means not private, loopback, link-local, etc.
return network.is_global or str(network) in ["0.0.0.0/0", "::/0"]
except (ValueError, TypeError):
return False
Whitespace-only changes.
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.openstack.services.blockstorage.blockstorage_service import (
BlockStorage,
)
blockstorage_client = BlockStorage(Provider.get_global_provider())
@@ -0,0 +1,172 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, List
from openstack import exceptions as openstack_exceptions
from prowler.lib.logger import logger
from prowler.providers.openstack.lib.service.service import OpenStackService
class BlockStorage(OpenStackService):
"""Service wrapper using openstacksdk block storage (Cinder) APIs."""
def __init__(self, provider) -> None:
super().__init__(__class__.__name__, provider)
self.volumes: List[VolumeResource] = []
self.snapshots: List[SnapshotResource] = []
self.backups: List[BackupResource] = []
self._list_volumes()
self._list_snapshots()
self._list_backups()
def _list_volumes(self) -> None:
"""List all block storage volumes across all audited regions."""
logger.info("BlockStorage - Listing volumes...")
for region, conn in self.regional_connections.items():
try:
for volume in conn.block_storage.volumes():
attachments = getattr(volume, "attachments", []) or []
self.volumes.append(
VolumeResource(
id=getattr(volume, "id", ""),
name=getattr(volume, "name", ""),
status=getattr(volume, "status", ""),
size=getattr(volume, "size", 0),
volume_type=getattr(volume, "volume_type", ""),
is_encrypted=getattr(volume, "is_encrypted", False),
is_bootable=str(
getattr(volume, "is_bootable", "false")
).lower()
== "true",
is_multiattach=getattr(volume, "is_multiattach", False),
attachments=attachments,
metadata=getattr(volume, "metadata", {}),
availability_zone=getattr(volume, "availability_zone", ""),
snapshot_id=getattr(volume, "snapshot_id", "") or "",
source_volume_id=getattr(volume, "source_volume_id", "")
or "",
project_id=self.project_id,
region=region,
)
)
except openstack_exceptions.SDKException as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- "
f"Failed to list block storage volumes in region {region}: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- "
f"Unexpected error listing block storage volumes in region {region}: {error}"
)
def _list_snapshots(self) -> None:
"""List all block storage snapshots across all audited regions."""
logger.info("BlockStorage - Listing snapshots...")
for region, conn in self.regional_connections.items():
try:
for snapshot in conn.block_storage.snapshots():
self.snapshots.append(
SnapshotResource(
id=getattr(snapshot, "id", ""),
name=getattr(snapshot, "name", ""),
status=getattr(snapshot, "status", ""),
size=getattr(snapshot, "size", 0),
volume_id=getattr(snapshot, "volume_id", ""),
metadata=getattr(snapshot, "metadata", {}),
project_id=self.project_id,
region=region,
)
)
except openstack_exceptions.SDKException as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- "
f"Failed to list block storage snapshots in region {region}: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- "
f"Unexpected error listing block storage snapshots in region {region}: {error}"
)
def _list_backups(self) -> None:
"""List all block storage backups across all audited regions."""
logger.info("BlockStorage - Listing backups...")
for region, conn in self.regional_connections.items():
try:
for backup in conn.block_storage.backups():
self.backups.append(
BackupResource(
id=getattr(backup, "id", ""),
name=getattr(backup, "name", ""),
status=getattr(backup, "status", ""),
size=getattr(backup, "size", 0),
volume_id=getattr(backup, "volume_id", ""),
is_incremental=getattr(backup, "is_incremental", False),
availability_zone=getattr(backup, "availability_zone", ""),
project_id=self.project_id,
region=region,
)
)
except openstack_exceptions.SDKException as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- "
f"Failed to list block storage backups in region {region}: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- "
f"Unexpected error listing block storage backups in region {region}: {error}"
)
@dataclass
class VolumeResource:
"""Represents an OpenStack block storage volume."""
id: str
name: str
status: str
size: int
volume_type: str
is_encrypted: bool
is_bootable: bool
is_multiattach: bool
attachments: List[Dict]
metadata: Dict[str, str]
availability_zone: str
snapshot_id: str
source_volume_id: str
project_id: str
region: str
@dataclass
class SnapshotResource:
"""Represents an OpenStack block storage snapshot."""
id: str
name: str
status: str
size: int
volume_id: str
metadata: Dict[str, str]
project_id: str
region: str
@dataclass
class BackupResource:
"""Represents an OpenStack block storage backup."""
id: str
name: str
status: str
size: int
volume_id: str
is_incremental: bool
availability_zone: str
project_id: str
region: str
@@ -0,0 +1,40 @@
{
"Provider": "openstack",
"CheckID": "blockstorage_snapshot_metadata_sensitive_data",
"CheckTitle": "Block storage snapshot metadata does not contain sensitive data",
"CheckType": [],
"ServiceName": "blockstorage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "OS::Cinder::Snapshot",
"ResourceGroup": "storage",
"Description": "**OpenStack block storage snapshot metadata** is evaluated to detect **sensitive data** such as passwords, API keys, secrets, and private keys. Snapshot metadata is accessible via the OpenStack API to any user with access to the project. Storing secrets in metadata exposes them to unauthorized access through the API.",
"Risk": "Snapshot metadata containing sensitive data exposes credentials through the OpenStack API, accessible to any project member. Attackers with project access can enumerate snapshot metadata to extract passwords, API keys, and private keys. Stolen credentials enable unauthorized modifications, privilege escalation, and data breaches.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.openstack.org/cinder/latest/cli/cli-manage-volumes.html",
"https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"
],
"Remediation": {
"Code": {
"CLI": "openstack volume snapshot unset --property <sensitive_key> <snapshot_id>",
"NativeIaC": "",
"Other": "1. Navigate to **Block Storage > Snapshots**\n2. Select snapshot with sensitive metadata\n3. Remove sensitive metadata keys using CLI command\n4. Rotate exposed credentials immediately\n5. Store secrets in Barbican or external secrets manager instead",
"Terraform": ""
},
"Recommendation": {
"Text": "Never store secrets in snapshot metadata; use Barbican (OpenStack Key Manager), Vault, or external secrets management instead. Remove any sensitive data currently stored in snapshot metadata and rotate exposed credentials immediately. Implement metadata policies to prevent sensitive data from being added.",
"Url": "https://hub.prowler.com/check/blockstorage_snapshot_metadata_sensitive_data"
}
},
"Categories": [
"secrets",
"encryption"
],
"DependsOn": [],
"RelatedTo": [
"blockstorage_volume_metadata_sensitive_data"
],
"Notes": "This check uses the detect-secrets library to scan for credentials. May produce false positives on metadata keys containing secret-like keywords. Findings should be reviewed manually. The audit_config allows configuring secrets_ignore_patterns to exclude specific patterns and detect_secrets_plugins to customize detection."
}
@@ -0,0 +1,62 @@
import json
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.lib.utils.utils import detect_secrets_scan
from prowler.providers.openstack.services.blockstorage.blockstorage_client import (
blockstorage_client,
)
class blockstorage_snapshot_metadata_sensitive_data(Check):
"""Ensure block storage snapshot metadata does not contain sensitive data like passwords or API keys."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
secrets_ignore_patterns = blockstorage_client.audit_config.get(
"secrets_ignore_patterns", []
)
for snapshot in blockstorage_client.snapshots:
report = CheckReportOpenStack(metadata=self.metadata(), resource=snapshot)
report.status = "PASS"
report.status_extended = f"Snapshot {snapshot.name} ({snapshot.id}) metadata does not contain sensitive data."
if snapshot.metadata:
# Build metadata dict and parallel list of keys
dump_metadata = {}
original_metadata_keys = []
for key, value in snapshot.metadata.items():
dump_metadata[key] = value
original_metadata_keys.append(key)
# Convert metadata dict to JSON string for detect-secrets scanning
metadata_json = json.dumps(dump_metadata, indent=2)
detect_secrets_output = detect_secrets_scan(
data=metadata_json,
excluded_secrets=secrets_ignore_patterns,
detect_secrets_plugins=blockstorage_client.audit_config.get(
"detect_secrets_plugins"
),
)
if detect_secrets_output:
# Map line numbers back to metadata keys using the parallel list
# Line numbering: line 1 = "{", line 2 = first key-value, etc.
secrets_string = ", ".join(
[
f"{secret['type']} in metadata key '{original_metadata_keys[secret['line_number'] - 2]}'"
for secret in detect_secrets_output
if 0
<= secret["line_number"] - 2
< len(original_metadata_keys)
]
)
report.status = "FAIL"
report.status_extended = f"Snapshot {snapshot.name} ({snapshot.id}) metadata contains potential secrets -> {secrets_string}."
else:
report.status_extended = f"Snapshot {snapshot.name} ({snapshot.id}) has no metadata (no sensitive data exposure risk)."
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "openstack",
"CheckID": "blockstorage_snapshot_not_orphaned",
"CheckTitle": "Block storage snapshots reference existing volumes",
"CheckType": [],
"ServiceName": "blockstorage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "OS::Cinder::Snapshot",
"ResourceGroup": "storage",
"Description": "**OpenStack block storage snapshots** are evaluated to verify they **reference existing volumes**. Orphaned snapshots (whose source volumes have been deleted) may contain stale data, incur unnecessary storage costs, and can be overlooked during security reviews. They may also contain sensitive data from deleted volumes that is no longer being managed.",
"Risk": "Orphaned snapshots may contain sensitive data from deleted volumes that is no longer actively managed or monitored. These snapshots continue to consume storage resources and may be restored by unauthorized users to access old data. They can be overlooked during security audits and compliance reviews.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.openstack.org/cinder/latest/cli/cli-manage-volumes.html"
],
"Remediation": {
"Code": {
"CLI": "openstack volume snapshot list\nopenstack volume snapshot delete <snapshot_id>",
"NativeIaC": "",
"Other": "1. Navigate to **Block Storage > Snapshots**\n2. Identify snapshots whose source volumes no longer exist\n3. Review each orphaned snapshot for necessity\n4. Back up data if needed by creating a volume from the snapshot\n5. Delete orphaned snapshots that are no longer needed",
"Terraform": ""
},
"Recommendation": {
"Text": "Review orphaned snapshots regularly and delete those no longer needed. Before deleting a volume, review and clean up associated snapshots. Implement snapshot lifecycle policies to prevent accumulation of orphaned snapshots. Tag snapshots with ownership and purpose metadata.",
"Url": "https://hub.prowler.com/check/blockstorage_snapshot_not_orphaned"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Orphaned snapshots may be intentionally retained for data recovery or compliance purposes. This check identifies snapshots referencing non-existent volumes for review. Organizations should evaluate each orphaned snapshot based on retention policies."
}
@@ -0,0 +1,32 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.providers.openstack.services.blockstorage.blockstorage_client import (
blockstorage_client,
)
class blockstorage_snapshot_not_orphaned(Check):
"""Ensure block storage snapshots reference existing volumes."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
# Build set of existing volume IDs
existing_volume_ids = {volume.id for volume in blockstorage_client.volumes}
for snapshot in blockstorage_client.snapshots:
report = CheckReportOpenStack(metadata=self.metadata(), resource=snapshot)
if snapshot.volume_id in existing_volume_ids:
report.status = "PASS"
report.status_extended = f"Snapshot {snapshot.name} ({snapshot.id}) references existing volume {snapshot.volume_id}."
else:
report.status = "FAIL"
report.status_extended = (
f"Snapshot {snapshot.name} ({snapshot.id}) references non-existent volume "
f"{snapshot.volume_id} and may be orphaned."
)
findings.append(report)
return findings
@@ -0,0 +1,36 @@
{
"Provider": "openstack",
"CheckID": "blockstorage_volume_backup_exists",
"CheckTitle": "Block storage volumes have at least one backup",
"CheckType": [],
"ServiceName": "blockstorage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OS::Cinder::Volume",
"ResourceGroup": "storage",
"Description": "**OpenStack block storage volumes** are evaluated to verify that at least one **backup** exists. Volume backups provide disaster recovery capability by storing volume data in a separate storage backend (e.g., Swift or Ceph). Without backups, data loss from volume corruption, accidental deletion, or infrastructure failure is irrecoverable.",
"Risk": "Volumes without backups are vulnerable to permanent data loss from hardware failures, accidental deletion, software bugs, or ransomware attacks. Without backups, recovery from disasters requires rebuilding data from scratch, which may be impossible for stateful applications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.openstack.org/cinder/latest/admin/volume-backups.html"
],
"Remediation": {
"Code": {
"CLI": "openstack volume backup create --name <backup_name> <volume_id>",
"NativeIaC": "",
"Other": "1. Navigate to **Block Storage > Volumes**\n2. Select the volume to back up\n3. Click **Create Backup**\n4. Provide a name and optional description\n5. Set up automated backup schedules using cron or orchestration tools",
"Terraform": ""
},
"Recommendation": {
"Text": "Create regular backups for all block storage volumes, especially those containing critical data. Implement automated backup schedules. Use incremental backups to reduce storage costs and backup time. Test backup restoration regularly to ensure recoverability.",
"Url": "https://hub.prowler.com/check/blockstorage_volume_backup_exists"
}
},
"Categories": [
"forensics-ready"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "The backup service must be enabled in the Cinder configuration. Backups are stored in a separate backend (typically Swift or Ceph). This check verifies the existence of at least one backup, not backup freshness or scheduling."
}
@@ -0,0 +1,37 @@
from collections import Counter
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.providers.openstack.services.blockstorage.blockstorage_client import (
blockstorage_client,
)
class blockstorage_volume_backup_exists(Check):
"""Ensure block storage volumes have at least one backup."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
# Build volume_id -> backup count mapping
backup_counts = Counter(
backup.volume_id for backup in blockstorage_client.backups
)
for volume in blockstorage_client.volumes:
report = CheckReportOpenStack(metadata=self.metadata(), resource=volume)
count = backup_counts.get(volume.id, 0)
if count > 0:
report.status = "PASS"
report.status_extended = (
f"Volume {volume.name} ({volume.id}) has {count} backup(s)."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Volume {volume.name} ({volume.id}) does not have any backups."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "openstack",
"CheckID": "blockstorage_volume_encryption_enabled",
"CheckTitle": "Block storage volumes have encryption enabled",
"CheckType": [],
"ServiceName": "blockstorage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OS::Cinder::Volume",
"ResourceGroup": "storage",
"Description": "**OpenStack block storage volumes** (Cinder) are evaluated to verify that **encryption** is enabled. Volume encryption protects data at rest by encrypting the entire volume using a key managed by the OpenStack Key Manager (Barbican). Without encryption, data stored on volumes is vulnerable to unauthorized access if the underlying storage is compromised.",
"Risk": "Unencrypted volumes expose data at rest to unauthorized access if physical storage media is compromised, stolen, or improperly decommissioned. Attackers with access to the storage backend can read sensitive data directly. Compliance frameworks (PCI-DSS, HIPAA, SOC2) require encryption of data at rest.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.openstack.org/cinder/latest/configuration/block-storage/volume-encryption.html",
"https://docs.openstack.org/barbican/latest/"
],
"Remediation": {
"Code": {
"CLI": "openstack volume type create --encryption-provider luks --encryption-cipher aes-xts-plain64 --encryption-key-size 256 --encryption-control-location front-end encrypted_type\nopenstack volume create --size 10 --type encrypted_type encrypted-volume",
"NativeIaC": "",
"Other": "1. Navigate to **Block Storage > Volumes**\n2. Create a new volume using an encrypted volume type\n3. Migrate data from unencrypted volumes to encrypted ones\n4. Delete the old unencrypted volumes\n\nNote: Existing volumes cannot be encrypted in-place; data must be migrated.",
"Terraform": "```hcl\nresource \"openstack_blockstorage_volume_v3\" \"encrypted_volume\" {\n name = \"encrypted-volume\"\n size = 10\n volume_type = openstack_blockstorage_volume_type_v3.encrypted.name\n}\n```"
},
"Recommendation": {
"Text": "Enable encryption on all block storage volumes using encrypted volume types backed by Barbican key management. Create encrypted volume types with strong ciphers (aes-xts-plain64) and adequate key sizes (256-bit). Migrate existing unencrypted volumes to encrypted ones.",
"Url": "https://hub.prowler.com/check/blockstorage_volume_encryption_enabled"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Volume encryption requires Barbican (OpenStack Key Manager) to be deployed and configured. Encryption is set at the volume type level and applies to all volumes created with that type. Existing volumes cannot be encrypted in-place."
}
@@ -0,0 +1,28 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.providers.openstack.services.blockstorage.blockstorage_client import (
blockstorage_client,
)
class blockstorage_volume_encryption_enabled(Check):
"""Ensure block storage volumes have encryption enabled."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
for volume in blockstorage_client.volumes:
report = CheckReportOpenStack(metadata=self.metadata(), resource=volume)
if volume.is_encrypted:
report.status = "PASS"
report.status_extended = (
f"Volume {volume.name} ({volume.id}) has encryption enabled."
)
else:
report.status = "FAIL"
report.status_extended = f"Volume {volume.name} ({volume.id}) does not have encryption enabled."
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "openstack",
"CheckID": "blockstorage_volume_metadata_sensitive_data",
"CheckTitle": "Block storage volume metadata does not contain sensitive data",
"CheckType": [],
"ServiceName": "blockstorage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "OS::Cinder::Volume",
"ResourceGroup": "storage",
"Description": "**OpenStack block storage volume metadata** is evaluated to detect **sensitive data** such as passwords, API keys, secrets, and private keys. Volume metadata is accessible via the OpenStack API to any user with access to the project. Storing secrets in metadata exposes them to unauthorized access through the API.",
"Risk": "Volume metadata containing sensitive data exposes credentials through the OpenStack API, accessible to any project member. Attackers with project access can enumerate volume metadata to extract passwords, API keys, and private keys. Stolen credentials enable unauthorized modifications, privilege escalation, and data breaches.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.openstack.org/cinder/latest/cli/cli-manage-volumes.html",
"https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"
],
"Remediation": {
"Code": {
"CLI": "openstack volume unset --property <sensitive_key> <volume_id>",
"NativeIaC": "",
"Other": "1. Navigate to **Block Storage > Volumes**\n2. Select volume with sensitive metadata\n3. Remove sensitive metadata keys using CLI command\n4. Rotate exposed credentials immediately\n5. Store secrets in Barbican or external secrets manager instead",
"Terraform": "```hcl\n# Use Barbican for secrets instead of volume metadata\nresource \"openstack_blockstorage_volume_v3\" \"secure_volume\" {\n name = \"app-data\"\n size = 10\n\n # Safe metadata (non-sensitive labels only)\n metadata = {\n environment = \"production\"\n application = \"web-app\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Never store secrets in volume metadata; use Barbican (OpenStack Key Manager), Vault, or external secrets management instead. Remove any sensitive data currently stored in volume metadata and rotate exposed credentials immediately. Implement metadata policies to prevent sensitive data from being added.",
"Url": "https://hub.prowler.com/check/blockstorage_volume_metadata_sensitive_data"
}
},
"Categories": [
"secrets",
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check uses the detect-secrets library to scan for credentials. May produce false positives on metadata keys containing secret-like keywords. Findings should be reviewed manually. The audit_config allows configuring secrets_ignore_patterns to exclude specific patterns and detect_secrets_plugins to customize detection."
}
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more