Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1201dcfd5 | ||
|
|
bc3cdd492a | ||
|
|
0366325539 | ||
|
|
071d6476e2 | ||
|
|
8573efc53b | ||
|
|
62988821a7 | ||
|
|
7a712d5fef | ||
|
|
8a3d27139a | ||
|
|
73415e2f8a | ||
|
|
e8d2b4a189 | ||
|
|
b61b6cba53 | ||
|
|
71ee4213b3 | ||
|
|
e96ea54f3b | ||
|
|
dfca97633e | ||
|
|
3538e7accf | ||
|
|
548a137046 | ||
|
|
012fd84cb0 | ||
|
|
8f3e69f571 | ||
|
|
9c2cb5efa8 | ||
|
|
fa93cabc0b | ||
|
|
efcbbf63c2 | ||
|
|
150abce4a8 | ||
|
|
dcf74113fc | ||
|
|
42f9b5fb2f | ||
|
|
c74fa131ea | ||
|
|
07dea4f402 | ||
|
|
c71ae75c70 | ||
|
|
b21ded6d46 | ||
|
|
8eddb48b16 | ||
|
|
d3ba93f0c0 |
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.20.0
|
||||
version: 1.20.1
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 575 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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("/")
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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."
|
||||
}
|
||||