chore: resolve conflicts

This commit is contained in:
HugoPBrito
2025-07-22 16:07:55 +02:00
189 changed files with 5087 additions and 1267 deletions
@@ -6,6 +6,7 @@ on:
- "master"
paths:
- "api/**"
- "prowler/**"
- ".github/workflows/api-build-lint-push-containers.yml"
# Uncomment the code below to test this action on PRs
@@ -19,12 +19,23 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
- name: Install Poetry
run: |
python3 -m pip install --user poetry
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Parse version and determine branch
run: |
# Validate version format (reusing pattern from sdk-bump-version.yml)
@@ -107,11 +118,12 @@ jobs:
echo "✓ api/pyproject.toml version: $CURRENT_API_VERSION"
- name: Verify prowler dependency in api/pyproject.toml
if: ${{ env.PATCH_VERSION != '0' }}
run: |
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
PROWLER_VERSION_TRIMMED=$(echo "$PROWLER_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_PROWLER_REF" != "$PROWLER_VERSION_TRIMMED" ]; then
echo "ERROR: Prowler dependency mismatch in api/pyproject.toml (expected: '$PROWLER_VERSION_TRIMMED', found: '$CURRENT_PROWLER_REF')"
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
if [ "$CURRENT_PROWLER_REF" != "$BRANCH_NAME_TRIMMED" ]; then
echo "ERROR: Prowler dependency mismatch in api/pyproject.toml (expected: '$BRANCH_NAME_TRIMMED', found: '$CURRENT_PROWLER_REF')"
exit 1
fi
echo "✓ api/pyproject.toml prowler dependency: $CURRENT_PROWLER_REF"
@@ -136,6 +148,36 @@ jobs:
fi
git checkout -b "$BRANCH_NAME"
- name: Update prowler dependency in api/pyproject.toml
if: ${{ env.PATCH_VERSION == '0' }}
run: |
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
# Minor release: update the dependency to use the new branch
echo "Minor release detected - updating prowler dependency from '$CURRENT_PROWLER_REF' to '$BRANCH_NAME_TRIMMED'"
sed -i "s|prowler @ git+https://github.com/prowler-cloud/prowler.git@[^\"]*\"|prowler @ git+https://github.com/prowler-cloud/prowler.git@$BRANCH_NAME_TRIMMED\"|" api/pyproject.toml
# Verify the change was made
UPDATED_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
if [ "$UPDATED_PROWLER_REF" != "$BRANCH_NAME_TRIMMED" ]; then
echo "ERROR: Failed to update prowler dependency in api/pyproject.toml"
exit 1
fi
# Update poetry lock file
echo "Updating poetry.lock file..."
cd api
poetry lock --no-update
cd ..
# Commit and push the changes
git add api/pyproject.toml api/poetry.lock
git commit -m "chore(api): update prowler dependency to $BRANCH_NAME_TRIMMED for release $PROWLER_VERSION"
git push origin "$BRANCH_NAME"
echo "✓ api/pyproject.toml prowler dependency updated to: $UPDATED_PROWLER_REF"
- name: Extract changelog entries
run: |
set -e
@@ -206,6 +248,7 @@ jobs:
name: Prowler ${{ env.PROWLER_VERSION }}
body_path: combined_changelog.md
draft: true
target_commitish: ${{ env.PATCH_VERSION == '0' && 'master' || env.BRANCH_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+16 -1
View File
@@ -2,7 +2,21 @@
All notable changes to the **Prowler API** are documented in this file.
## [v1.10.0] (Prowler UNRELEASED)
## [1.10.2] (Prowler v5.9.2)
### Changed
- Optimized queries for resources views [(#8336)](https://github.com/prowler-cloud/prowler/pull/8336)
---
## [v1.10.1] (Prowler v5.9.1)
### Fixed
- Calculate failed findings during scans to prevent heavy database queries [(#8322)](https://github.com/prowler-cloud/prowler/pull/8322)
---
## [v1.10.0] (Prowler v5.9.0)
### Added
- SSO with SAML support [(#8175)](https://github.com/prowler-cloud/prowler/pull/8175)
@@ -12,6 +26,7 @@ All notable changes to the **Prowler API** are documented in this file.
- `/processors` endpoints to post-process findings. Currently, only the Mutelist processor is supported to allow to mute findings.
- Optimized the underlying queries for resources endpoints [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- Optimized include parameters for resources view [(#8229)](https://github.com/prowler-cloud/prowler/pull/8229)
- Optimized overview background tasks [(#8300)](https://github.com/prowler-cloud/prowler/pull/8300)
### Fixed
- Search filter for findings and resources [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
+1 -1
View File
@@ -38,7 +38,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.10.0"
version = "1.10.2"
[project.scripts]
celery = "src.backend.config.settings.celery"
+23
View File
@@ -175,6 +175,29 @@ def create_objects_in_batches(
model.objects.bulk_create(chunk, batch_size)
def update_objects_in_batches(
tenant_id: str, model, objects: list, fields: list, batch_size: int = 500
):
"""
Bulk-update model instances in repeated, per-tenant RLS transactions.
All chunks execute in their own transaction, so no single transaction
grows too large.
Args:
tenant_id (str): UUID string of the tenant under which to set RLS.
model: Django model class whose `.objects.bulk_update()` will be called.
objects (list): List of model instances (saved) to bulk-update.
fields (list): List of field names to update.
batch_size (int): Maximum number of objects per bulk_update call.
"""
total = len(objects)
for start in range(0, total, batch_size):
chunk = objects[start : start + batch_size]
with rls_transaction(value=tenant_id, parameter=POSTGRES_TENANT_VAR):
model.objects.bulk_update(chunk, fields, batch_size)
# Postgres Enums
@@ -0,0 +1,30 @@
from functools import partial
from django.db import migrations
from api.db_utils import create_index_on_partitions, drop_index_on_partitions
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0039_resource_resources_failed_findings_idx"),
]
operations = [
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="resource_finding_mappings",
index_name="rfm_tenant_resource_idx",
columns="tenant_id, resource_id",
method="BTREE",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="resource_finding_mappings",
index_name="rfm_tenant_resource_idx",
),
),
]
@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0040_rfm_tenant_resource_index_partitions"),
]
operations = [
migrations.AddIndex(
model_name="resourcefindingmapping",
index=models.Index(
fields=["tenant_id", "resource_id"],
name="rfm_tenant_resource_idx",
),
),
]
@@ -0,0 +1,23 @@
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0041_rfm_tenant_resource_parent_partitions"),
("django_celery_beat", "0019_alter_periodictasks_options"),
]
operations = [
AddIndexConcurrently(
model_name="scan",
index=models.Index(
condition=models.Q(("state", "completed")),
fields=["tenant_id", "provider_id", "-inserted_at"],
include=("id",),
name="scans_prov_ins_desc_idx",
),
),
]
+11
View File
@@ -476,6 +476,13 @@ class Scan(RowLevelSecurityProtectedModel):
condition=Q(state=StateChoices.COMPLETED),
name="scans_prov_state_ins_desc_idx",
),
# TODO This might replace `scans_prov_state_ins_desc_idx` completely. Review usage
models.Index(
fields=["tenant_id", "provider_id", "-inserted_at"],
condition=Q(state=StateChoices.COMPLETED),
include=["id"],
name="scans_prov_ins_desc_idx",
),
]
class JSONAPIMeta:
@@ -860,6 +867,10 @@ class ResourceFindingMapping(PostgresPartitionedModel, RowLevelSecurityProtected
fields=["tenant_id", "finding_id"],
name="rfm_tenant_finding_idx",
),
models.Index(
fields=["tenant_id", "resource_id"],
name="rfm_tenant_resource_idx",
),
]
constraints = [
models.UniqueConstraint(
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.10.0
version: 1.10.2
description: |-
Prowler API specification.
@@ -13,6 +13,7 @@ from api.db_utils import (
enum_to_choices,
generate_random_token,
one_week_from_now,
update_objects_in_batches,
)
from api.models import Provider
@@ -227,3 +228,88 @@ class TestCreateObjectsInBatches:
qs = Provider.objects.filter(tenant=tenant)
assert qs.count() == total
@pytest.mark.django_db
class TestUpdateObjectsInBatches:
@pytest.fixture
def tenant(self, tenants_fixture):
return tenants_fixture[0]
def make_provider_instances(self, tenant, count):
"""
Return a list of `count` unsaved Provider instances for the given tenant.
"""
base_uid = 2000
return [
Provider(
tenant=tenant,
uid=str(base_uid + i),
provider=Provider.ProviderChoices.AWS,
)
for i in range(count)
]
def test_exact_multiple_of_batch(self, tenant):
total = 6
batch_size = 3
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs, batch_size=batch_size)
# Fetch them back, mutate the `uid` field, then update in batches
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
batch_size=batch_size,
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
def test_non_multiple_of_batch(self, tenant):
total = 7
batch_size = 3
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs, batch_size=batch_size)
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
batch_size=batch_size,
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
def test_batch_size_default(self, tenant):
default_size = settings.DJANGO_DELETION_BATCH_SIZE
total = default_size + 2
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs)
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
# Update without specifying batch_size (uses default)
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
+2
View File
@@ -5188,6 +5188,8 @@ class TestComplianceOverviewViewSet:
assert "description" in attributes
assert "status" in attributes
# TODO: This test may fail randomly because requirements are not ordered
@pytest.mark.xfail
def test_compliance_overview_requirements_manual(
self, authenticated_client, compliance_requirements_overviews_fixture
):
+35 -8
View File
@@ -22,7 +22,7 @@ from django.conf import settings as django_settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.search import SearchQuery
from django.db import transaction
from django.db.models import Count, F, Prefetch, Q, Sum
from django.db.models import Count, F, Prefetch, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.http import HttpResponse
from django.shortcuts import redirect
@@ -292,7 +292,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.10.0"
spectacular_settings.VERSION = "1.10.2"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -1994,6 +1994,21 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
)
)
def _should_prefetch_findings(self) -> bool:
fields_param = self.request.query_params.get("fields[resources]", "")
include_param = self.request.query_params.get("include", "")
return (
fields_param == ""
or "findings" in fields_param.split(",")
or "findings" in include_param.split(",")
)
def _get_findings_prefetch(self):
findings_queryset = Finding.all_objects.defer("scan", "resources").filter(
tenant_id=self.request.tenant_id
)
return [Prefetch("findings", queryset=findings_queryset)]
def get_serializer_class(self):
if self.action in ["metadata", "metadata_latest"]:
return ResourceMetadataSerializer
@@ -2017,7 +2032,11 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
filtered_queryset,
manager=Resource.all_objects,
select_related=["provider"],
prefetch_related=["findings"],
prefetch_related=(
self._get_findings_prefetch()
if self._should_prefetch_findings()
else []
),
)
def retrieve(self, request, *args, **kwargs):
@@ -2042,14 +2061,18 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
tenant_id = request.tenant_id
filtered_queryset = self.filter_queryset(self.get_queryset())
latest_scan_ids = (
Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
latest_scans = (
Scan.all_objects.filter(
tenant_id=tenant_id,
state=StateChoices.COMPLETED,
)
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
.values("provider_id")
)
filtered_queryset = filtered_queryset.filter(
tenant_id=tenant_id, provider__scan__in=latest_scan_ids
provider_id__in=Subquery(latest_scans)
)
return self.paginate_by_pk(
@@ -2057,7 +2080,11 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
filtered_queryset,
manager=Resource.all_objects,
select_related=["provider"],
prefetch_related=["findings"],
prefetch_related=(
self._get_findings_prefetch()
if self._should_prefetch_findings()
else []
),
)
@action(detail=False, methods=["get"], url_name="metadata")
+55 -61
View File
@@ -1,19 +1,24 @@
import json
import time
from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timezone
from celery.utils.log import get_task_logger
from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS
from django.db import IntegrityError, OperationalError
from django.db.models import Case, Count, IntegerField, OuterRef, Subquery, Sum, When
from django.db.models import Case, Count, IntegerField, Prefetch, Sum, When
from tasks.utils import CustomEncoder
from api.compliance import (
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
generate_scan_compliance,
)
from api.db_utils import create_objects_in_batches, rls_transaction
from api.db_utils import (
create_objects_in_batches,
rls_transaction,
update_objects_in_batches,
)
from api.exceptions import ProviderConnectionError
from api.models import (
ComplianceRequirementOverview,
@@ -103,7 +108,10 @@ def _store_resources(
def perform_prowler_scan(
tenant_id: str, scan_id: str, provider_id: str, checks_to_execute: list[str] = None
tenant_id: str,
scan_id: str,
provider_id: str,
checks_to_execute: list[str] | None = None,
):
"""
Perform a scan using Prowler and store the findings and resources in the database.
@@ -175,6 +183,7 @@ def perform_prowler_scan(
resource_cache = {}
tag_cache = {}
last_status_cache = {}
resource_failed_findings_cache = defaultdict(int)
for progress, findings in prowler_scan.scan():
for finding in findings:
@@ -200,6 +209,9 @@ def perform_prowler_scan(
},
)
resource_cache[resource_uid] = resource_instance
# Initialize all processed resources in the cache
resource_failed_findings_cache[resource_uid] = 0
else:
resource_instance = resource_cache[resource_uid]
@@ -313,6 +325,11 @@ def perform_prowler_scan(
)
finding_instance.add_resources([resource_instance])
# Increment failed_findings_count cache if the finding status is FAIL and not muted
if status == FindingStatus.FAIL and not finding.muted:
resource_uid = finding.resource_uid
resource_failed_findings_cache[resource_uid] += 1
# Update scan resource summaries
scan_resource_cache.add(
(
@@ -330,6 +347,24 @@ def perform_prowler_scan(
scan_instance.state = StateChoices.COMPLETED
# Update failed_findings_count for all resources in batches if scan completed successfully
if resource_failed_findings_cache:
resources_to_update = []
for resource_uid, failed_count in resource_failed_findings_cache.items():
if resource_uid in resource_cache:
resource_instance = resource_cache[resource_uid]
resource_instance.failed_findings_count = failed_count
resources_to_update.append(resource_instance)
if resources_to_update:
update_objects_in_batches(
tenant_id=tenant_id,
model=Resource,
objects=resources_to_update,
fields=["failed_findings_count"],
batch_size=1000,
)
except Exception as e:
logger.error(f"Error performing scan {scan_id}: {e}")
exception = e
@@ -376,7 +411,6 @@ def perform_prowler_scan(
def aggregate_findings(tenant_id: str, scan_id: str):
"""
Aggregates findings for a given scan and stores the results in the ScanSummary table.
Also updates the failed_findings_count for each resource based on the latest findings.
This function retrieves all findings associated with a given `scan_id` and calculates various
metrics such as counts of failed, passed, and muted findings, as well as their deltas (new,
@@ -405,8 +439,6 @@ def aggregate_findings(tenant_id: str, scan_id: str):
- muted_new: Muted findings with a delta of 'new'.
- muted_changed: Muted findings with a delta of 'changed'.
"""
_update_resource_failed_findings_count(tenant_id, scan_id)
with rls_transaction(tenant_id):
findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id)
@@ -531,53 +563,6 @@ def aggregate_findings(tenant_id: str, scan_id: str):
ScanSummary.objects.bulk_create(scan_aggregations, batch_size=3000)
def _update_resource_failed_findings_count(tenant_id: str, scan_id: str):
"""
Update the failed_findings_count field for resources based on the latest findings.
This function calculates the number of failed findings for each resource by:
1. Getting the latest finding for each finding.uid
2. Counting failed findings per resource
3. Updating the failed_findings_count field for each resource
Args:
tenant_id (str): The ID of the tenant to which the scan belongs.
scan_id (str): The ID of the scan for which to update resource counts.
"""
with rls_transaction(tenant_id):
scan = Scan.objects.get(pk=scan_id)
provider_id = scan.provider_id
resources = list(
Resource.all_objects.filter(tenant_id=tenant_id, provider_id=provider_id)
)
# For each resource, calculate failed findings count based on latest findings
for resource in resources:
with rls_transaction(tenant_id):
# Get the latest finding for each finding.uid that affects this resource
latest_findings_subquery = (
Finding.all_objects.filter(
tenant_id=tenant_id, uid=OuterRef("uid"), resources=resource
)
.order_by("-inserted_at")
.values("id")[:1]
)
# Count failed findings from the latest findings
failed_count = Finding.all_objects.filter(
tenant_id=tenant_id,
resources=resource,
id__in=Subquery(latest_findings_subquery),
status=FindingStatus.FAIL,
muted=False,
).count()
resource.failed_findings_count = failed_count
resource.save(update_fields=["failed_findings_count"])
def create_compliance_requirements(tenant_id: str, scan_id: str):
"""
Create detailed compliance requirement overview records for a scan.
@@ -603,18 +588,27 @@ def create_compliance_requirements(tenant_id: str, scan_id: str):
prowler_provider = return_prowler_provider(provider_instance)
# Get check status data by region from findings
findings = (
Finding.all_objects.filter(scan_id=scan_id, muted=False)
.only("id", "check_id", "status")
.prefetch_related(
Prefetch(
"resources",
queryset=Resource.objects.only("id", "region"),
to_attr="small_resources",
)
)
.iterator(chunk_size=1000)
)
check_status_by_region = {}
with rls_transaction(tenant_id):
findings = Finding.objects.filter(scan_id=scan_id, muted=False)
for finding in findings:
# Get region from resources
for resource in finding.resources.all():
for resource in finding.small_resources:
region = resource.region
region_dict = check_status_by_region.setdefault(region, {})
current_status = region_dict.get(finding.check_id)
if current_status == "FAIL":
continue
region_dict[finding.check_id] = finding.status
current_status = check_status_by_region.setdefault(region, {})
if current_status.get(finding.check_id) != "FAIL":
current_status[finding.check_id] = finding.status
try:
# Try to get regions from provider
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,234 @@
from locust import events, task
from utils.config import (
L_PROVIDER_NAME,
M_PROVIDER_NAME,
RESOURCES_UI_FIELDS,
S_PROVIDER_NAME,
TARGET_INSERTED_AT,
)
from utils.helpers import (
APIUserBase,
get_api_token,
get_auth_headers,
get_dynamic_filters_pairs,
get_next_resource_filter,
get_scan_id_from_provider_name,
)
GLOBAL = {
"token": None,
"scan_ids": {},
"resource_filters": None,
"large_resource_filters": None,
}
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
GLOBAL["token"] = get_api_token(environment.host)
GLOBAL["scan_ids"]["small"] = get_scan_id_from_provider_name(
environment.host, GLOBAL["token"], S_PROVIDER_NAME
)
GLOBAL["scan_ids"]["medium"] = get_scan_id_from_provider_name(
environment.host, GLOBAL["token"], M_PROVIDER_NAME
)
GLOBAL["scan_ids"]["large"] = get_scan_id_from_provider_name(
environment.host, GLOBAL["token"], L_PROVIDER_NAME
)
GLOBAL["resource_filters"] = get_dynamic_filters_pairs(
environment.host, GLOBAL["token"], "resources"
)
GLOBAL["large_resource_filters"] = get_dynamic_filters_pairs(
environment.host, GLOBAL["token"], "resources", GLOBAL["scan_ids"]["large"]
)
class APIUser(APIUserBase):
def on_start(self):
self.token = GLOBAL["token"]
self.s_scan_id = GLOBAL["scan_ids"]["small"]
self.m_scan_id = GLOBAL["scan_ids"]["medium"]
self.l_scan_id = GLOBAL["scan_ids"]["large"]
self.available_resource_filters = GLOBAL["resource_filters"]
self.available_resource_filters_large_scan = GLOBAL["large_resource_filters"]
@task
def resources_default(self):
name = "/resources"
page_number = self._next_page(name)
endpoint = (
f"/resources?page[number]={page_number}"
f"&filter[updated_at]={TARGET_INSERTED_AT}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_default_ui_fields(self):
name = "/resources?fields"
page_number = self._next_page(name)
endpoint = (
f"/resources?page[number]={page_number}"
f"&fields[resources]={','.join(RESOURCES_UI_FIELDS)}"
f"&filter[updated_at]={TARGET_INSERTED_AT}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_default_include(self):
name = "/resources?include"
page = self._next_page(name)
endpoint = (
f"/resources?page[number]={page}"
f"&filter[updated_at]={TARGET_INSERTED_AT}"
f"&include=provider"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_metadata(self):
name = "/resources/metadata"
endpoint = f"/resources/metadata?filter[updated_at]={TARGET_INSERTED_AT}"
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task
def resources_scan_small(self):
name = "/resources?filter[scan_id] - 50k"
page_number = self._next_page(name)
endpoint = (
f"/resources?page[number]={page_number}" f"&filter[scan]={self.s_scan_id}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task
def resources_metadata_scan_small(self):
name = "/resources/metadata?filter[scan_id] - 50k"
endpoint = f"/resources/metadata?&filter[scan]={self.s_scan_id}"
self.client.get(
endpoint,
headers=get_auth_headers(self.token),
name=name,
)
@task(2)
def resources_scan_medium(self):
name = "/resources?filter[scan_id] - 250k"
page_number = self._next_page(name)
endpoint = (
f"/resources?page[number]={page_number}" f"&filter[scan]={self.m_scan_id}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task
def resources_metadata_scan_medium(self):
name = "/resources/metadata?filter[scan_id] - 250k"
endpoint = f"/resources/metadata?&filter[scan]={self.m_scan_id}"
self.client.get(
endpoint,
headers=get_auth_headers(self.token),
name=name,
)
@task
def resources_scan_large(self):
name = "/resources?filter[scan_id] - 500k"
page_number = self._next_page(name)
endpoint = (
f"/resources?page[number]={page_number}" f"&filter[scan]={self.l_scan_id}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task
def resources_scan_large_include(self):
name = "/resources?filter[scan_id]&include - 500k"
page_number = self._next_page(name)
endpoint = (
f"/resources?page[number]={page_number}"
f"&filter[scan]={self.l_scan_id}"
f"&include=provider"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task
def resources_metadata_scan_large(self):
endpoint = f"/resources/metadata?&filter[scan]={self.l_scan_id}"
self.client.get(
endpoint,
headers=get_auth_headers(self.token),
name="/resources/metadata?filter[scan_id] - 500k",
)
@task(2)
def resources_filters(self):
name = "/resources?filter[resource_filter]&include"
filter_name, filter_value = get_next_resource_filter(
self.available_resource_filters
)
endpoint = (
f"/resources?filter[{filter_name}]={filter_value}"
f"&filter[updated_at]={TARGET_INSERTED_AT}"
f"&include=provider"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_metadata_filters(self):
name = "/resources/metadata?filter[resource_filter]"
filter_name, filter_value = get_next_resource_filter(
self.available_resource_filters
)
endpoint = (
f"/resources/metadata?filter[{filter_name}]={filter_value}"
f"&filter[updated_at]={TARGET_INSERTED_AT}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_metadata_filters_scan_large(self):
name = "/resources/metadata?filter[resource_filter]&filter[scan_id] - 500k"
filter_name, filter_value = get_next_resource_filter(
self.available_resource_filters
)
endpoint = (
f"/resources/metadata?filter[{filter_name}]={filter_value}"
f"&filter[scan]={self.l_scan_id}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(2)
def resourcess_filter_large_scan_include(self):
name = "/resources?filter[resource_filter][scan]&include - 500k"
filter_name, filter_value = get_next_resource_filter(
self.available_resource_filters
)
endpoint = (
f"/resources?filter[{filter_name}]={filter_value}"
f"&filter[scan]={self.l_scan_id}"
f"&include=provider"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_latest_default_ui_fields(self):
name = "/resources/latest?fields"
page_number = self._next_page(name)
endpoint = (
f"/resources/latest?page[number]={page_number}"
f"&fields[resources]={','.join(RESOURCES_UI_FIELDS)}"
)
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
@task(3)
def resources_latest_metadata_filters(self):
name = "/resources/metadata/latest?filter[resource_filter]"
filter_name, filter_value = get_next_resource_filter(
self.available_resource_filters
)
endpoint = f"/resources/metadata/latest?filter[{filter_name}]={filter_value}"
self.client.get(endpoint, headers=get_auth_headers(self.token), name=name)
+17
View File
@@ -13,6 +13,23 @@ FINDINGS_RESOURCE_METADATA = {
"resource_types": "resource_type",
"services": "service",
}
RESOURCE_METADATA = {
"regions": "region",
"types": "type",
"services": "service",
}
RESOURCES_UI_FIELDS = [
"name",
"failed_findings_count",
"region",
"service",
"type",
"provider",
"inserted_at",
"updated_at",
"uid",
]
S_PROVIDER_NAME = "provider-50k"
M_PROVIDER_NAME = "provider-250k"
+16 -6
View File
@@ -7,6 +7,7 @@ from locust import HttpUser, between
from utils.config import (
BASE_HEADERS,
FINDINGS_RESOURCE_METADATA,
RESOURCE_METADATA,
TARGET_INSERTED_AT,
USER_EMAIL,
USER_PASSWORD,
@@ -121,13 +122,16 @@ def get_scan_id_from_provider_name(host: str, token: str, provider_name: str) ->
return response.json()["data"][0]["id"]
def get_resource_filters_pairs(host: str, token: str, scan_id: str = "") -> dict:
def get_dynamic_filters_pairs(
host: str, token: str, endpoint: str, scan_id: str = ""
) -> dict:
"""
Retrieves and maps resource metadata filter values from the findings endpoint.
Retrieves and maps metadata filter values from a given endpoint.
Args:
host (str): The host URL of the API.
token (str): Bearer token for authentication.
endpoint (str): The API endpoint to query for metadata.
scan_id (str, optional): Optional scan ID to filter metadata. Defaults to using inserted_at timestamp.
Returns:
@@ -136,22 +140,28 @@ def get_resource_filters_pairs(host: str, token: str, scan_id: str = "") -> dict
Raises:
AssertionError: If the request fails or does not return a 200 status code.
"""
metadata_mapping = (
FINDINGS_RESOURCE_METADATA if endpoint == "findings" else RESOURCE_METADATA
)
date_filter = "inserted_at" if endpoint == "findings" else "updated_at"
metadata_filters = (
f"filter[scan]={scan_id}"
if scan_id
else f"filter[inserted_at]={TARGET_INSERTED_AT}"
else f"filter[{date_filter}]={TARGET_INSERTED_AT}"
)
response = requests.get(
f"{host}/findings/metadata?{metadata_filters}", headers=get_auth_headers(token)
f"{host}/{endpoint}/metadata?{metadata_filters}",
headers=get_auth_headers(token),
)
assert (
response.status_code == 200
), f"Failed to get resource filters values: {response.text}"
attributes = response.json()["data"]["attributes"]
return {
FINDINGS_RESOURCE_METADATA[key]: values
metadata_mapping[key]: values
for key, values in attributes.items()
if key in FINDINGS_RESOURCE_METADATA.keys()
if key in metadata_mapping.keys()
}
+7 -7
View File
@@ -1,6 +1,6 @@
# Extending Prowler Lighthouse
# Extending Prowler Lighthouse AI
This guide helps developers customize and extend Prowler Lighthouse by adding or modifying AI agents.
This guide helps developers customize and extend Prowler Lighthouse AI by adding or modifying AI agents.
## Understanding AI Agents
@@ -13,7 +13,7 @@ AI agents fall into two main categories:
- **Autonomous Agents**: Freely chooses from available tools to complete tasks, adapting their approach based on context. They decide which tools to use and when.
- **Workflow Agents**: Follows structured paths with predefined logic. They execute specific tool sequences and can include conditional logic.
Prowler Lighthouse is an autonomous agent - selecting the right tool(s) based on the users query.
Prowler Lighthouse AI is an autonomous agent - selecting the right tool(s) based on the users query.
???+ note
To learn more about AI agents, read [Anthropic's blog post on building effective agents](https://www.anthropic.com/engineering/building-effective-agents).
@@ -24,15 +24,15 @@ The autonomous nature of agents depends on the underlying LLM. Autonomous agents
After evaluating multiple LLM providers (OpenAI, Gemini, Claude, LLama) based on tool calling features and response accuracy, we recommend using the `gpt-4o` model.
## Prowler Lighthouse Architecture
## Prowler Lighthouse AI Architecture
Prowler Lighthouse uses a multi-agent architecture orchestrated by the [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library.
Prowler Lighthouse AI uses a multi-agent architecture orchestrated by the [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library.
### Architecture Components
<img src="../../tutorials/img/lighthouse-architecture.png" alt="Prowler Lighthouse architecture">
Prowler Lighthouse integrates with the NextJS application:
Prowler Lighthouse AI integrates with the NextJS application:
- The [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library integrates directly with NextJS
- The system uses the authenticated user session to interact with the Prowler API server
@@ -74,7 +74,7 @@ Modifying the supervisor prompt allows you to:
The supervisor agent and all specialized agents are defined in the `route.ts` file. The supervisor agent uses [langgraph-supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor), while other agents use the prebuilt [create-react-agent](https://langchain-ai.github.io/langgraphjs/how-tos/create-react-agent/).
To add new capabilities or all Lighthouse to interact with other APIs, create additional specialized agents:
To add new capabilities or all Lighthouse AI to interact with other APIs, create additional specialized agents:
1. First determine what the new agent would do. Create a detailed prompt defining the agent's purpose and capabilities. You can see an example from [here](https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts#L359-L385).
???+ note
Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+45
View File
@@ -312,6 +312,51 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
prowler azure --az-cli-auth
```
### Prowler App Update
You have two options to upgrade your Prowler App installation:
#### Option 1: Change env file with the following values
Edit your `.env` file and change the version values:
```env
PROWLER_UI_VERSION="5.9.0"
PROWLER_API_VERSION="5.9.0"
```
#### Option 2: Run the following command
```bash
docker compose pull --policy always
```
The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally.
???+ note "What Gets Preserved During Upgrade"
Everything is preserved, nothing will be deleted after the update.
#### Troubleshooting
If containers don't start, check logs for errors:
```bash
# Check logs for errors
docker compose logs
# Verify image versions
docker images | grep prowler
```
If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running:
```bash
docker compose pull
docker compose up -d
```
## Prowler container versions
The available versions of Prowler CLI are the following:
+1
View File
@@ -78,6 +78,7 @@ The following list includes all the Azure checks with configurable variables tha
| `app_ensure_python_version_is_latest` | `python_latest_version` | String |
| `app_ensure_java_version_is_latest` | `java_latest_version` | String |
| `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings |
| `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String |
## GCP
+20 -20
View File
@@ -1,12 +1,12 @@
# Prowler Lighthouse
# Prowler Lighthouse AI
Prowler Lighthouse is an AI Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
Prowler Lighthouse AI is a Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
<img src="../img/lighthouse-intro.png" alt="Prowler Lighthouse">
## How It Works
Prowler Lighthouse uses OpenAI's language models and integrates with your Prowler security findings data.
Prowler Lighthouse AI uses OpenAI's language models and integrates with your Prowler security findings data.
Here's what's happening behind the scenes:
@@ -14,28 +14,28 @@ Here's what's happening behind the scenes:
- It uses a ["supervisor" architecture](https://langchain-ai.lang.chat/langgraphjs/tutorials/multi_agent/agent_supervisor/) that interacts with different agents for specialized tasks. For example, `findings_agent` can analyze detected security findings, while `overview_agent` provides a summary of connected cloud accounts.
- The system connects to OpenAI models to understand, fetch the right data, and respond to the user's query.
???+ note
Lighthouse is tested against `gpt-4o` and `gpt-4o-mini` OpenAI models.
Lighthouse AI is tested against `gpt-4o` and `gpt-4o-mini` OpenAI models.
- The supervisor agent is the main contact point. It is what users interact with directly from the chat interface. It coordinates with other agents to answer users' questions comprehensively.
<img src="../img/lighthouse-architecture.png" alt="Lighthouse Architecture">
<img src="../img/lighthouse-architecture.png" alt="Lighthouse AI Architecture">
???+ note
All agents can only read relevant security data. They cannot modify your data or access sensitive information like configured secrets or tenant details.
## Set up
Getting started with Prowler Lighthouse is easy:
Getting started with Prowler Lighthouse AI is easy:
1. Go to the configuration page in your Prowler dashboard.
2. Enter your OpenAI API key.
3. Select your preferred model. The recommended one for best results is `gpt-4o`.
4. (Optional) Add business context to improve response quality and prioritization.
<img src="../img/lighthouse-config.png" alt="Lighthouse Configuration">
<img src="../img/lighthouse-config.png" alt="Lighthouse AI Configuration">
### Adding Business Context
The optional business context field lets you provide additional information to help Lighthouse understand your environment and priorities, including:
The optional business context field lets you provide additional information to help Lighthouse AI understand your environment and priorities, including:
- Your organization's cloud security goals
- Information about account owners or responsible teams
@@ -46,7 +46,7 @@ Better context leads to more relevant responses and prioritization that aligns w
## Capabilities
Prowler Lighthouse is designed to be your AI security team member, with capabilities including:
Prowler Lighthouse AI is designed to be your AI security team member, with capabilities including:
### Natural Language Querying
@@ -70,7 +70,7 @@ Get tailored step-by-step instructions for fixing security issues:
### Enhanced Context and Analysis
Lighthouse can provide additional context to help you understand the findings:
Lighthouse AI can provide additional context to help you understand the findings:
- Explain security concepts related to findings in simple terms
- Provide risk assessments based on your environment and context
@@ -82,20 +82,20 @@ Lighthouse can provide additional context to help you understand the findings:
## Important Notes
Prowler Lighthouse is powerful, but there are limitations:
Prowler Lighthouse AI is powerful, but there are limitations:
- **Continuous improvement**: Please report any issues, as the feature may make mistakes or encounter errors, despite extensive testing.
- **Access limitations**: Lighthouse can only access data the logged-in user can view. If you can't see certain information, Lighthouse can't see it either.
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse will error out. Refresh and log back in to continue.
- **Access limitations**: Lighthouse AI can only access data the logged-in user can view. If you can't see certain information, Lighthouse AI can't see it either.
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
- **Response quality**: The response quality depends on the selected OpenAI model. For best results, use gpt-4o.
### Getting Help
If you encounter issues with Prowler Lighthouse or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
If you encounter issues with Prowler Lighthouse AI or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
### What Data Is Shared to OpenAI?
The following API endpoints are accessible to Prowler Lighthouse. Data from the following API endpoints could be shared with OpenAI depending on the scope of user's query:
The following API endpoints are accessible to Prowler Lighthouse AI. Data from the following API endpoints could be shared with OpenAI depending on the scope of user's query:
#### Accessible API Endpoints
@@ -139,7 +139,7 @@ The following API endpoints are accessible to Prowler Lighthouse. Data from the
#### Excluded API Endpoints
Not all Prowler API endpoints are integrated with Lighthouse. They are intentionally excluded for the following reasons:
Not all Prowler API endpoints are integrated with Lighthouse AI. They are intentionally excluded for the following reasons:
- OpenAI/other LLM providers shouldn't have access to sensitive data (like fetching provider secrets and other sensitive config)
- Users queries don't need responses from those API endpoints (ex: tasks, tenant details, downloading zip file, etc.)
@@ -173,7 +173,7 @@ Not all Prowler API endpoints are integrated with Lighthouse. They are intention
- List all tasks - `/api/v1/tasks`
- Retrieve data from a specific task - `/api/v1/tasks/{id}`
**Lighthouse Configuration:**
**Lighthouse AI Configuration:**
- List OpenAI configuration - `/api/v1/lighthouse-config`
- Retrieve OpenAI key and configuration - `/api/v1/lighthouse-config/{id}`
@@ -187,7 +187,7 @@ Not all Prowler API endpoints are integrated with Lighthouse. They are intention
During feature development, we evaluated other LLM models.
- **Claude AI** - Claude models have [tier-based ratelimits](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). For Lighthouse to answer slightly complex questions, there are a handful of API calls to the LLM provider within few seconds. With Claude's tiering system, users must purchase $400 credits or convert their subscription to monthly invoicing after talking to their sales team. This pricing may not suit all Prowler users.
- **Claude AI** - Claude models have [tier-based ratelimits](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). For Lighthouse AI to answer slightly complex questions, there are a handful of API calls to the LLM provider within few seconds. With Claude's tiering system, users must purchase $400 credits or convert their subscription to monthly invoicing after talking to their sales team. This pricing may not suit all Prowler users.
- **Gemini Models** - Gemini lacks a solid tool calling feature like OpenAI. It calls functions recursively until exceeding limits. Gemini-2.5-Pro-Experimental is better than previous models regarding tool calling and responding, but it's still experimental.
- **Deepseek V3** - Doesn't support system prompt messages.
@@ -197,8 +197,8 @@ Context windows are limited. While demo data fits inside the context window, que
**3. Is my security data shared with OpenAI?**
Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The Lighthouse key is only accessible to our NextJS server and is never sent to LLMs. Resource metadata (names, tags, account/project IDs, etc) may be shared with OpenAI based on your query requirements.
Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The OpenAI key configured with Lighthouse AI is only accessible to our NextJS server and is never sent to LLMs. Resource metadata (names, tags, account/project IDs, etc) may be shared with OpenAI based on your query requirements.
**4. Can the Lighthouse change my cloud environment?**
**4. Can the Lighthouse AI change my cloud environment?**
No. The agent doesn't have the tools to make the changes, even if the configured cloud provider API keys contain permissions to modify resources.
@@ -0,0 +1,59 @@
# Mute Findings (Mutelist)
Prowler App allows users to mute specific findings to focus on the most critical security issues. This comprehensive guide demonstrates how to effectively use the Mutelist feature to manage and prioritize security findings.
## What Is the Mutelist Feature?
The Mutelist feature enables users to:
- **Suppress specific findings** from appearing in future scans
- **Focus on critical issues** by hiding resolved or accepted risks
- **Maintain audit trails** of muted findings for compliance purposes
- **Streamline security workflows** by reducing noise from non-critical findings
## Prerequisites
Before muting findings, ensure:
- Valid access to Prowler App with appropriate permissions
- A provider added to the Prowler App
- Understanding of the security implications of muting specific findings
???+ warning
Muting findings does not resolve underlying security issues. Review each finding carefully before muting to ensure it represents an acceptable risk or has been properly addressed.
## Step 1: Add a provider
To configure Mutelist:
1. Log into Prowler App
2. Navigate to the providers page
![Add provider](../img/mutelist-ui-1.png)
3. Add a provider, then "Configure Muted Findings" button will be enabled in providers page and scans page
![Button enabled in providers page](../img/mutelist-ui-2.png)
![Button enabled in scans pages](../img/mutelist-ui-3.png)
## Step 2: Configure Mutelist
1. Open the modal by clicking "Configure Muted Findings" button
![Open modal](../img/mutelist-ui-4.png)
1. Provide a valid Mutelist in `YAML` format. More details about Mutelist [here](../tutorials/mutelist.md)
![Valid YAML configuration](../img/mutelist-ui-5.png)
If the YAML configuration is invalid, an error message will be displayed
![Wrong YAML configuration](../img/mutelist-ui-7.png)
![Wrong YAML configuration 2](../img/mutelist-ui-8.png)
## Step 3: Review the Mutelist
1. Once added, the configuration can be removed or updated
![Remove or update configuration](../img/mutelist-ui-6.png)
## Step 4: Check muted findings in the scan results
1. Run a new scan
2. Check the muted findings in the scan results
![Check muted fidings](../img/mutelist-ui-9.png)
???+ note
The Mutelist configuration takes effect on the next scans.
+43
View File
@@ -0,0 +1,43 @@
# Entra ID Configuration
This page provides instructions for creating and configuring a Microsoft Entra ID (formerly Azure AD) application to use SAML SSO with Prowler App.
## Creating and Configuring the Enterprise Application
1. From the "Enterprise Applications" page in the Azure Portal, click "+ New application".
![New application](../img/saml/saml-sso-azure-1.png)
2. At the top of the page, click "+ Create your own application".
![Create application](../img/saml/saml-sso-azure-2.png)
3. Enter a name for the application and select the "Integrate any other application you don't find in the gallery (Non-gallery)" option.
![Enter name](../img/saml/saml-sso-azure-3.png)
4. Assign users and groups to the application, then proceed to "Set up single sign on" and select "SAML" as the method.
![Select SAML](../img/saml/saml-sso-azure-4.png)
5. In the "Basic SAML Configuration" section, click "Edit".
![Edit](../img/saml/saml-sso-azure-5.png)
6. Enter the "Identifier (Entity ID)" and "Reply URL (Assertion Consumer Service URL)". These values can be obtained from the SAML SSO integration setup in Prowler App. For detailed instructions, refer to the [SAML SSO Configuration](./prowler-app-sso.md) page.
![Enter data](../img/saml/saml-sso-azure-6.png)
7. In the "SAML Certificates" section, click "Edit".
![Edit](../img/saml/saml-sso-azure-7.png)
8. For the "Signing Option," select "Sign SAML response and assertion", and then click "Save".
![Signing options](../img/saml/saml-sso-azure-8.png)
9. Once the changes are saved, the metadata XML can be downloaded from the "App Federation Metadata Url".
![Metadata XML](../img/saml/saml-sso-azure-9.png)
10. Save the downloaded Metadata XML to a file. To complete the setup, upload this file during the Prowler App integration. (See the [SAML SSO Configuration](./prowler-app-sso.md) page for details).
+141 -113
View File
@@ -1,175 +1,203 @@
# Configuring SAML Single Sign-On (SSO) in Prowler
# SAML Single Sign-On (SSO) Configuration
This guide explains how to enable and test SAML SSO integration in Prowler. It includes environment setup, API endpoints, and how to configure Okta as your Identity Provider (IdP).
This guide provides comprehensive instructions to configure SAML-based Single Sign-On (SSO) in Prowler App. This configuration allows users to authenticate using the organization's Identity Provider (IdP).
This document is divided into two main sections:
- **User Guide**: For organization administrators to configure SAML SSO through Prowler App.
- **Developer and Administrator Guide**: For developers and system administrators running self-hosted Prowler App instances, providing technical details on environment configuration, API usage, and testing.
---
## Environment Configuration
## User Guide: Configuring SAML SSO in Prowler App
### `DJANGO_ALLOWED_HOSTS`
Follow these steps to enable and configure SAML SSO for an organization.
Update this variable to specify which domains Django should accept incoming requests from. This typically includes:
### Key Features
- `localhost` for local development
- container hostnames (e.g. `prowler-api`)
- public-facing domains or tunnels (e.g. ngrok)
Prowler can be integrated with SAML SSO identity providers such as Okta to enable single sign-on for the organization's users. The Prowler SAML integration currently supports the following features:
**Example**:
- **IdP-Initiated SSO**: Users can initiate login from their Identity Provider's dashboard.
- **SP-Initiated SSO**: Users can initiate login directly from the Prowler login page.
- **Just-in-Time Provisioning**: Users from the organization signing into Prowler for the first time will be automatically created.
```env
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api,mycompany.prowler
```
???+ warning "Deactivate SAML"
If the SAML configuration is removed, users who previously authenticated via SAML will need to reset their password to regain access using standard login. This is because their accounts no longer have valid authentication credentials without the SAML integration.
# SAML Configuration API
### Prerequisites
You can manage SAML settings via the API. Prowler provides full CRUD support for tenant-specific SAML configuration.
- Administrator access to the Prowler organization is required.
- Administrative access to the SAML 2.0 compliant Identity Provider (e.g., Okta, Azure AD, Google Workspace) is necessary.
- GET /api/v1/saml-config: Retrieve the current configuration
### Configuration Steps
- POST /api/v1/saml-config: Create a new configuration
#### Step 1: Access Profile Settings
- PATCH /api/v1/saml-config: Update the existing configuration
To access the account settings, click the "Account" button in the top-right corner of Prowler App, or navigate directly to `https://cloud.prowler.com/profile` (or `http://localhost:3000/profile` for local setups).
- DELETE /api/v1/saml-config: Remove the current configuration
![Access Profile Settings](../img/saml/saml-step-1.png)
#### Step 2: Enable SAML Integration
???+ note "API Note"
SSO with SAML API documentation.[Prowler API Reference - Upload SAML configuration](https://api.prowler.com/api/v1/docs#tag/SAML/operation/saml_config_create)
On the profile page, find the "SAML SSO Integration" card and click "Enable" to begin the configuration process.
# SAML Initiate
![Enable SAML Integration](../img/saml/saml-step-2.png)
### Description
#### Step 3: Configure the Identity Provider (IdP)
This endpoint receives an email and checks if there is an active SAML configuration for the associated domain (i.e., the part after the @). If a configuration exists it responds with an HTTP 302 redirect to the appropriate saml_login endpoint for the organization.
The Prowler SAML configuration panel displays the information needed to configure the IdP. This information must be used to create a new SAML application in the IdP.
- POST /api/v1/accounts/saml/initiate/
1. **Assertion Consumer Service (ACS) URL**: The endpoint in Prowler that will receive the SAML assertion from the IdP.
2. **Audience URI (Entity ID)**: A unique identifier for the Prowler application (Service Provider).
???+ note
Important: This endpoint is intended to be used from a browser, as it returns a 302 redirect that needs to be followed to continue the SAML authentication flow. For testing purposes, it is better to use a browser or a tool that follows redirects (such as Postman) rather than relying on unit tests that cannot capture the redirect behavior.
To configure the IdP, copy the **ACS URL** and **Audience URI** from Prowler and use them to set up a new SAML application.
### Expected payload
```
{
"email_domain": "user@domain.com"
}
```
![IdP configuration](../img/saml/idp_config.png)
### Possible responses
???+ info "IdP Configuration"
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application. For SSO integration with Azure AD / Entra ID, see our [Entra ID configuration instructions](./prowler-app-sso-entra.md).
• 302 FOUND: Redirects to the SAML login URL associated with the organization.
#### Step 4: Configure Attribute Mapping in the IdP
• 403 FORBIDDEN: The domain is not authorized.
For Prowler to correctly identify and provision users, the IdP must be configured to send the following attributes in the SAML assertion:
### Validation logic
| Attribute Name | Description | Required |
|----------------|---------------------------------------------------------------------------------------------------------|----------|
| `firstName` | The user's first name. | Yes |
| `lastName` | The user's last name. | Yes |
| `userType` | The Prowler role to be assigned to the user (e.g., `admin`, `auditor`). If a role with that name already exists, it will be used; otherwise, a new role called `no_permissions` will be created with minimal permissions. You can then edit the permissions for that role in the [RBAC Management tab](./prowler-app-rbac.md). | No |
| `companyName` | The user's company name. This is automatically populated if the IdP sends an `organization` attribute. | No |
• Looks up the domain in SAMLDomainIndex.
???+ info "IdP Attribute Mapping"
Note that the attribute name is just an example and may be different in your IdP. For instance, if your IdP provides a 'division' attribute, you can map it to 'userType'.
![IdP configuration](../img/saml/saml_attribute_statements.png)
• Retrieves the related SAMLConfiguration object via tenant_id.
???+ warning "Dynamic Updates"
These attributes are updated in Prowler each time a user logs in. Any changes made in the identity provider (IdP) will be reflected the next time the user logs in again.
#### Step 5: Upload IdP Metadata to Prowler
# SAML Integration: Testing Guide
Once the IdP is configured, it provides a **metadata XML file**. This file contains the IdP's configuration information, such as its public key and login URL.
This document outlines the process for testing the SAML integration functionality.
To complete the Prowler-side configuration:
1. Return to the Prowler SAML configuration page.
2. Enter the **email domain** for the organization (e.g., `mycompany.com`). Prowler uses this to identify users who should authenticate via SAML.
3. Upload the **metadata XML file** downloaded from the IdP.
![Configure Prowler with IdP Metadata](../img/saml/saml-step-3.png)
#### Step 6: Save and Verify Configuration
Click the "Save" button to complete the setup. The "SAML Integration" card will now show an "Active" status, indicating that the configuration is complete and enabled.
![Verify Integration Status](../img/saml/saml-step-4.png)
???+ info "IdP Configuration"
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application.
##### Remove SAML Configuration
You can disable SAML SSO by removing the existing configuration from the integration panel.
![Remove SAML configuration](../img/saml/saml-step-remove.png)
### Signing in with SAML SSO
Once SAML SSO is enabled, users from the configured domain can sign in by entering their email address on the login page and clicking "Continue with SAML SSO". They will be redirected to the IdP to authenticate and then returned to Prowler.
![Sign in with SAML SSO](../img/saml/saml-step-5.png)
---
## 1. Start Ngrok and Update ALLOWED_HOSTS
## Developer and Administrator Guide
Start ngrok on port 8080:
```
This section provides technical details for developers and administrators of self-hosted Prowler instances.
### Environment Configuration
For self-hosted deployments, several environment variables must be configured to ensure SAML SSO functions correctly. These variables are typically set in an `.env` file.
| Variable | Description | Example |
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| `API_BASE_URL` | The base URL of the Prowler API instance. | `http://mycompany.prowler/api/v1` |
| `DJANGO_ALLOWED_HOSTS` | A comma-separated list of hostnames that the Django backend will accept requests from. Include any domains used to access the Prowler API. | `localhost,127.0.0.1,prowler-api,mycompany.prowler` |
| `AUTH_URL` | The base URL of the Prowler web UI. This is used to construct the callback URL after authentication. | `http://mycompany.prowler` |
| `SAML_SSO_CALLBACK_URL` | The full callback URL where users are redirected after authenticating with the IdP. It is typically constructed using the `AUTH_URL`. | `${AUTH_URL}/api/auth/callback/saml` |
After modifying these variables, the Prowler API must be restarted for the changes to take effect.
### SAML API Reference
Prowler provides a REST API to manage SAML configurations programmatically.
- **Endpoint**: `/api/v1/saml-config`
- **Methods**:
- `GET`: Retrieve the current SAML configuration for the tenant.
- `POST`: Create a new SAML configuration.
- `PATCH`: Update an existing SAML configuration.
- `DELETE`: Remove the SAML configuration.
???+ note "API Documentation"
For detailed information on using the API, refer to the [Prowler API Reference](https://api.prowler.com/api/v1/docs#tag/SAML/operation/saml_config_create).
#### SAML Initiate Endpoint
- **Endpoint**: `POST /api/v1/accounts/saml/initiate/`
- **Description**: This endpoint initiates the SAML login flow. It takes an email address, determines if the domain has a SAML configuration, and redirects the user to the appropriate IdP login page. It is primarily designed for browser-based flows.
### Testing SAML Integration
Follow these steps to test a SAML integration in a development environment.
#### 1. Expose the Local Environment
Since the IdP needs to send requests to the local Prowler instance, it must be exposed to the internet. A tool like `ngrok` can be used for this purpose.
To start ngrok, run the following command:
```bash
ngrok http 8080
```
This command provides a public URL (e.g., `https://<random-string>.ngrok.io`) that forwards to the local server on port 8080.
Then, copy the generated ngrok URL and include it in the ALLOWED_HOSTS setting. If you're using the development environment, it usually defaults to *, but in some cases this may not work properly, like in my tests (investigate):
#### 2. Update `DJANGO_ALLOWED_HOSTS`
```
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])
To allow requests from ngrok, add its URL to the `DJANGO_ALLOWED_HOSTS` environment variable.
```env
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api,*.ngrok.io
```
## 2. Configure the Identity Provider (IdP)
#### 3. Configure the IdP
Start your environment and configure your IdP. You will need to download the IdP's metadata XML file.
When configuring the IdP for testing, use the ngrok URL for the ACS URL:
`https://<your-ngrok-url>/api/v1/accounts/saml/<YOUR_DOMAIN>/acs/`
Your Assertion Consumer Service (ACS) URL must follow this format:
#### 4. Configure Prowler via API
```
https://<PROXY_URL>/api/v1/accounts/saml/<CONFIGURED_DOMAIN>/acs/
```
## 3. IdP Attribute Mapping
The following fields are expected from the IdP:
- firstName
- lastName
- userType (this is the name of the role the user should be assigned)
- companyName (this is filled automatically if the IdP includes an "organization" field)
These values are dynamic. If the values change in the IdP, they will be updated on the next login.
## 4. SAML Configuration API (POST)
SAML configuration is managed via a CRUD API. Use the following POST request to create a new configuration:
To create a SAML configuration for testing, use `curl`. Make sure to replace placeholders with actual data.
```bash
curl --location 'http://localhost:8080/api/v1/saml-config' \
--header 'Content-Type: application/vnd.api+json' \
--header 'Accept: application/vnd.api+json' \
--header 'Authorization: Bearer <TOKEN>' \
--header 'Authorization: Bearer <YOUR_API_TOKEN>' \
--data '{
"data": {
"type": "saml-configurations",
"attributes": {
"email_domain": "prowler.com",
"metadata_xml": "<XML>"
"email_domain": "yourdomain.com",
"metadata_xml": "<PASTE_YOUR_IDP_METADATA_XML_HERE>"
}
}
}'
```
## 5. SAML SSO Callback Configuration
#### 5. Initiate Login Flow
### Environment Variable Configuration
To test the end-to-end flow, construct the login URL and open it in a browser. This will start the IdP-initiated login flow.
The SAML authentication flow requires proper callback URL configuration to handle post-authentication redirects. Configure the following environment variables:
`https://<your-ngrok-url>/api/v1/accounts/saml/<YOUR_DOMAIN>/login/`
#### `SAML_SSO_CALLBACK_URL`
Specifies the callback endpoint that will be invoked upon successful SAML authentication completion. This URL directs users back to the web application interface.
```env
SAML_SSO_CALLBACK_URL="${AUTH_URL}/api/auth/callback/saml"
```
#### `AUTH_URL`
Defines the base URL of the web user interface application that serves as the authentication callback destination.
```env
AUTH_URL="<WEB_UI_URL>"
```
### Configuration Notes
- The `SAML_SSO_CALLBACK_URL` dynamically references the `AUTH_URL` variable to construct the complete callback endpoint
- Ensure the `AUTH_URL` points to the correct web UI deployment (development, staging, or production)
- The callback endpoint `/api/auth/callback/saml` must be accessible and properly configured to handle SAML authentication responses
- Both environment variables are required for proper SAML SSO functionality
- Verify that the `NEXT_PUBLIC_API_BASE_URL` environment variable is properly configured to reference the correct API server base URL corresponding to your target deployment environment. This ensures proper routing of SAML callback requests to the appropriate backend services.
## 6. Start SAML Login Flow
Once everything is configured, start the SAML login process by visiting the following URL:
```
https://<PROXY_IP>/api/v1/accounts/saml/<CONFIGURED_DOMAIN>/login/?email=<USER_EMAIL>
```
At the end you will get a valid access and refresh token
## 7. Notes on the initiate Endpoint
The initiate endpoint is not strictly required. It was created to allow extra checks or behavior modifications (like enumeration mitigation). It also simplifies UI integration with SAML, but again, it's optional.
If successful, the user will be redirected back to the Prowler application with a valid session.
+1
View File
@@ -54,6 +54,7 @@ nav:
- Role-Based Access Control: tutorials/prowler-app-rbac.md
- Social Login: tutorials/prowler-app-social-login.md
- SSO with SAML: tutorials/prowler-app-sso.md
- Mute findings: tutorials/prowler-app-mute-findings.md
- Lighthouse: tutorials/prowler-app-lighthouse.md
- CLI:
- Miscellaneous: tutorials/misc.md
+21 -7
View File
@@ -2,7 +2,21 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [v5.9.0] (Prowler UNRELEASED)
## [v5.10.0] (Prowler UNRELEASED)
### Added
- Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321)
---
## [v5.9.2] (Prowler v5.9.2)
### Fixed
- Use the correct resource name in `defender_domain_dkim_enabled` check [(#8334)](https://github.com/prowler-cloud/prowler/pull/8334)
---
## [v5.9.0] (Prowler v5.9.0)
### Added
- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123)
@@ -11,6 +25,8 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149)
- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168)
- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181)
- `defender_attack_path_notifications_properly_configured` check for Azure provider [(#8245)](https://github.com/prowler-cloud/prowler/pull/8245)
- `entra_intune_enrollment_sign_in_frequency_every_time` check for M365 provider [(#8223)](https://github.com/prowler-cloud/prowler/pull/8223)
- Support for remote repository scanning in IaC provider [(#8193)](https://github.com/prowler-cloud/prowler/pull/8193)
- Add `test_connection` method to GitHub provider [(#8248)](https://github.com/prowler-cloud/prowler/pull/8248)
@@ -20,18 +36,16 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
- Title & description wording for `iam_user_accesskey_unused` check for AWS provider [(#8233)](https://github.com/prowler-cloud/prowler/pull/8233)
- Add GitHub provider to lateral panel in documentation and change -h environment variable output [(#8246)](https://github.com/prowler-cloud/prowler/pull/8246)
- Show `m365_identity_type` and `m365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
- Ensure `is_service_role` only returns `True` for service roles [(#8274)](https://github.com/prowler-cloud/prowler/pull/8274)
- Update DynamoDB check metadata to fix broken link [(#8273)](https://github.com/prowler-cloud/prowler/pull/8273)
- Show correct count of findings in Dashboard Security Posture page [(#8270)](https://github.com/prowler-cloud/prowler/pull/8270)
- Add Check's metadata service name validator [(#8289)](https://github.com/prowler-cloud/prowler/pull/8289)
- Use subscription ID in Azure mutelist [(#8290)](https://github.com/prowler-cloud/prowler/pull/8290)
- `ServiceName` field in Network Firewall checks metadata [(#8280)](https://github.com/prowler-cloud/prowler/pull/8280)
- Update `entra_users_mfa_capable` check to use the correct resource name and ID [(#8288)](https://github.com/prowler-cloud/prowler/pull/8288)
---
## [v5.8.2] (Prowler UNRELEASED)
### Fixed
- Handle multiple services and severities while listing checks [(#8302)](https://github.com/prowler-cloud/prowler/pull/8302)
- Handle `tenant_id` for M365 Mutelist [(#8306)](https://github.com/prowler-cloud/prowler/pull/8306)
- Fix error in Dashboard Overview page when reading CSV files [(#8257)](https://github.com/prowler-cloud/prowler/pull/8257)
---
+1 -1
View File
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
timestamp = datetime.today()
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
prowler_version = "5.9.0"
prowler_version = "5.10.0"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
+4
View File
@@ -430,6 +430,10 @@ azure:
# TODO: create common config
shodan_api_key: null
# Configurable minimal risk level for attack path notifications
# azure.defender_attack_path_notifications_properly_configured
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.2"
+2
View File
@@ -635,6 +635,8 @@ def execute(
is_finding_muted_args["account_name"] = (
global_provider.identity.account_name
)
elif global_provider.type == "m365":
is_finding_muted_args["tenant_id"] = global_provider.identity.tenant_id
for finding in check_findings:
if global_provider.type == "azure":
is_finding_muted_args["subscription_id"] = (
+7 -8
View File
@@ -66,16 +66,15 @@ def load_checks_to_execute(
checks_to_execute.update(check_severities[severity])
if service_list:
checks_from_services = set()
for service in service_list:
checks_to_execute = (
set(
CheckMetadata.list(
bulk_checks_metadata=bulk_checks_metadata,
service=service,
)
)
& checks_to_execute
service_checks = CheckMetadata.list(
bulk_checks_metadata=bulk_checks_metadata,
service=service,
)
checks_from_services.update(service_checks)
checks_to_execute = checks_from_services & checks_to_execute
# Handle if there are checks passed using -C/--checks-file
elif checks_file:
checks_to_execute = parse_checks_from_file(checks_file, provider)
+30
View File
@@ -149,6 +149,36 @@ class CheckMetadata(BaseModel):
raise ValueError("ResourceType must be a non-empty string")
return resource_type
@validator("ServiceName", pre=True, always=True)
def validate_service_name(cls, service_name, values):
if not service_name:
raise ValueError("ServiceName must be a non-empty string")
check_id = values.get("CheckID")
if check_id:
service_from_check_id = check_id.split("_")[0]
if service_name != service_from_check_id:
raise ValueError(
f"ServiceName {service_name} does not belong to CheckID {check_id}"
)
if not service_name.islower():
raise ValueError(f"ServiceName {service_name} must be in lowercase")
return service_name
@validator("CheckID", pre=True, always=True)
def valid_check_id(cls, check_id):
if not check_id:
raise ValueError("CheckID must be a non-empty string")
if check_id:
if "-" in check_id:
raise ValueError(
f"CheckID {check_id} contains a hyphen, which is not allowed"
)
return check_id
@staticmethod
def get_bulk(provider: str) -> dict[str, "CheckMetadata"]:
"""
@@ -1417,6 +1417,11 @@
"bedrock-data-automation": {
"regions": {
"aws": [
"ap-south-1",
"ap-southeast-2",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-west-2"
],
@@ -2503,6 +2508,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -2544,6 +2550,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -2587,6 +2594,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -5075,6 +5083,7 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -5088,6 +5097,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -5994,6 +6004,7 @@
"aws": [
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -7396,6 +7407,8 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"eu-central-1",
"eu-central-2",
@@ -7492,6 +7505,7 @@
"aws": [
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -8181,6 +8195,7 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -9540,7 +9555,9 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
@@ -10090,6 +10107,7 @@
"aws": [
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -8,7 +8,7 @@
"CheckType": [
"IAM"
],
"ServiceName": "apigateway",
"ServiceName": "apigatewayv2",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "medium",
@@ -8,7 +8,7 @@
"CheckType": [
"Logging and Monitoring"
],
"ServiceName": "apigateway",
"ServiceName": "apigatewayv2",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "medium",
@@ -0,0 +1,36 @@
{
"Provider": "aws",
"CheckID": "bedrock_api_key_no_administrative_privileges",
"CheckTitle": "Ensure Amazon Bedrock API keys do not have administrative privileges or privilege escalation",
"CheckType": [
"Software and Configuration Checks",
"Industry and Regulatory Standards"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:iam:region:account-id:user/{user-name}/credential/{api-key-id}",
"Severity": "high",
"ResourceType": "AwsIamServiceSpecificCredential",
"Description": "Ensure that Amazon Bedrock API keys do not have administrative privileges or privilege escalation capabilities. API keys with administrative privileges can perform any action on any resource in your AWS environment, while privilege escalation allows users to grant themselves additional permissions, both posing significant security risks.",
"Risk": "Amazon Bedrock API keys with administrative privileges can perform any action on any resource in your AWS environment. Privilege escalation capabilities allow users to grant themselves additional permissions beyond their intended scope. Both violations of the principle of least privilege can lead to security vulnerabilities, data leaks, data loss, or unexpected charges if the API key is compromised or misused.",
"RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html",
"Remediation": {
"Code": {
"CLI": "aws iam delete-service-specific-credential --user-name <username> --service-specific-credential-id <credential-id>",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Apply the principle of least privilege to Amazon Bedrock API keys. Instead of granting administrative privileges or privilege escalation capabilities, assign only the permissions necessary for specific tasks. Create custom IAM policies with minimal permissions based on the principle of least privilege. Regularly review and audit API key permissions to ensure they cannot be used for privilege escalation.",
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege"
}
},
"Categories": [
"gen-ai",
"trustboundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check verifies that Amazon Bedrock API keys do not have administrative privileges or privilege escalation capabilities through attached IAM policies or inline policies. It follows the principle of least privilege to ensure API keys only have the minimum necessary permissions and cannot be used to escalate privileges."
}
@@ -0,0 +1,57 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.iam.iam_client import iam_client
from prowler.providers.aws.services.iam.lib.policy import (
check_admin_access,
check_full_service_access,
)
from prowler.providers.aws.services.iam.lib.privilege_escalation import (
check_privilege_escalation,
)
class bedrock_api_key_no_administrative_privileges(Check):
def execute(self):
findings = []
for api_key in iam_client.service_specific_credentials:
if api_key.service_name != "bedrock.amazonaws.com":
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=api_key)
report.status = "PASS"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has no administrative privileges."
for policy in api_key.user.attached_policies:
policy_arn = policy["PolicyArn"]
if policy_arn in iam_client.policies:
policy_document = iam_client.policies[policy_arn].document
if policy_document:
if check_admin_access(policy_document):
report.status = "FAIL"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has administrative privileges through attached policy {policy['PolicyName']}."
break
elif check_privilege_escalation(policy_document):
report.status = "FAIL"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has privilege escalation through attached policy {policy['PolicyName']}."
break
elif check_full_service_access("bedrock", policy_document):
report.status = "FAIL"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has full service access through attached policy {policy['PolicyName']}."
break
for inline_policy_name in api_key.user.inline_policies:
inline_policy_arn = f"{api_key.user.arn}:policy/{inline_policy_name}"
if inline_policy_arn in iam_client.policies:
policy_document = iam_client.policies[inline_policy_arn].document
if policy_document:
if check_admin_access(policy_document):
report.status = "FAIL"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has administrative privileges through inline policy {inline_policy_name}."
break
elif check_privilege_escalation(policy_document):
report.status = "FAIL"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has privilege escalation through inline policy {inline_policy_name}."
break
elif check_full_service_access("bedrock", policy_document):
report.status = "FAIL"
report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has full service access through inline policy {inline_policy_name}."
break
findings.append(report)
return findings
@@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access
class iam_aws_attached_policy_no_administrative_privileges(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only for attached AWS policies
if policy.attached and policy.type == "AWS":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access
class iam_customer_attached_policy_no_administrative_privileges(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only for attached custom policies
if policy.attached and policy.type == "Custom":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access
class iam_customer_unattached_policy_no_administrative_privileges(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only for cutomer unattached policies
if not policy.attached and policy.type == "Custom":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -9,7 +9,7 @@ class iam_inline_policy_allows_privilege_escalation(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
if policy.type == "Inline":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.resource_id = f"{policy.entity}/{policy.name}"
@@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access
class iam_inline_policy_no_administrative_privileges(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
if policy.type == "Inline":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
@@ -9,7 +9,7 @@ class iam_inline_policy_no_full_access_to_cloudtrail(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only inline policies
if policy.type == "Inline":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -9,7 +9,7 @@ class iam_inline_policy_no_full_access_to_kms(Check):
def execute(self):
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
if policy.type == "Inline":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
@@ -13,7 +13,7 @@ class iam_no_custom_policy_permissive_role_assumption(Check):
return any("*" in r for r in resource)
return False
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only custom policies
if policy.type == "Custom":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -9,7 +9,7 @@ class iam_policy_allows_privilege_escalation(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
if policy.type == "Custom":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
@@ -8,7 +8,7 @@ critical_service = "cloudtrail"
class iam_policy_no_full_access_to_cloudtrail(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only custom policies
if policy.type == "Custom":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -8,7 +8,7 @@ critical_service = "kms"
class iam_policy_no_full_access_to_kms(Check):
def execute(self) -> Check_Report_AWS:
findings = []
for policy in iam_client.policies:
for policy in iam_client.policies.values():
# Check only custom policies
if policy.type == "Custom":
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
@@ -77,13 +77,15 @@ class IAM(AWSService):
cloudshell_admin_policy_arn
)
# List both Customer (attached and unattached) and AWS Managed (only attached) policies
self.policies = []
self.policies.extend(self._list_policies("AWS"))
self.policies.extend(self._list_policies("Local"))
self.policies = {}
self.policies.update(self._list_policies("AWS"))
self.policies.update(self._list_policies("Local"))
self._list_policies_version(self.policies)
self._list_inline_user_policies()
self._list_inline_group_policies()
self._list_inline_role_policies()
self.service_specific_credentials = []
self._list_service_specific_credentials()
self.saml_providers = self._list_saml_providers()
self.server_certificates = self._list_server_certificates()
self.access_keys_metadata = {}
@@ -99,7 +101,7 @@ class IAM(AWSService):
self.__threading_call__(self._list_tags, self.roles)
self.__threading_call__(
self._list_tags,
[policy for policy in self.policies if policy.type == "Custom"],
[policy for policy in self.policies.values() if policy.type == "Custom"],
)
self.__threading_call__(self._list_tags, self.server_certificates)
if self.saml_providers is not None:
@@ -514,16 +516,15 @@ class IAM(AWSService):
UserName=user.name, PolicyName=policy
)
inline_user_policy_doc = inline_policy["PolicyDocument"]
self.policies.append(
Policy(
name=policy,
arn=user.arn,
entity=user.name,
type="Inline",
attached=True,
version_id="v1",
document=inline_user_policy_doc,
)
inline_user_policy_arn = f"{user.arn}:policy/{policy}"
self.policies[inline_user_policy_arn] = Policy(
name=policy,
arn=user.arn,
entity=user.name,
type="Inline",
attached=True,
version_id="v1",
document=inline_user_policy_doc,
)
except ClientError as error:
if error.response["Error"]["Code"] == "NoSuchEntity":
@@ -572,16 +573,15 @@ class IAM(AWSService):
GroupName=group.name, PolicyName=policy
)
inline_group_policy_doc = inline_policy["PolicyDocument"]
self.policies.append(
Policy(
name=policy,
arn=group.arn,
entity=group.name,
type="Inline",
attached=True,
version_id="v1",
document=inline_group_policy_doc,
)
inline_group_policy_arn = f"{group.arn}:policy/{policy}"
self.policies[inline_group_policy_arn] = Policy(
name=policy,
arn=group.arn,
entity=group.name,
type="Inline",
attached=True,
version_id="v1",
document=inline_group_policy_doc,
)
except ClientError as error:
if error.response["Error"]["Code"] == "NoSuchEntity":
@@ -633,16 +633,15 @@ class IAM(AWSService):
RoleName=role.name, PolicyName=policy
)
inline_role_policy_doc = inline_policy["PolicyDocument"]
self.policies.append(
Policy(
name=policy,
arn=role.arn,
entity=role.name,
type="Inline",
attached=True,
version_id="v1",
document=inline_role_policy_doc,
)
inline_role_policy_arn = f"{role.arn}:policy/{policy}"
self.policies[inline_role_policy_arn] = Policy(
name=policy,
arn=role.arn,
entity=role.name,
type="Inline",
attached=True,
version_id="v1",
document=inline_role_policy_doc,
)
except ClientError as error:
if error.response["Error"]["Code"] == "NoSuchEntity":
@@ -742,7 +741,7 @@ class IAM(AWSService):
def _list_policies(self, scope):
logger.info("IAM - List Policies...")
try:
policies = []
policies = {}
list_policies_paginator = self.client.get_paginator("list_policies")
for page in list_policies_paginator.paginate(
Scope=scope, OnlyAttached=False if scope == "Local" else True
@@ -751,17 +750,13 @@ class IAM(AWSService):
if not self.audit_resources or (
is_resource_filtered(policy["Arn"], self.audit_resources)
):
policies.append(
Policy(
name=policy["PolicyName"],
arn=policy["Arn"],
entity=policy["PolicyId"],
version_id=policy["DefaultVersionId"],
type="Custom" if scope == "Local" else "AWS",
attached=(
True if policy["AttachmentCount"] > 0 else False
),
)
policies[policy["Arn"]] = Policy(
name=policy["PolicyName"],
arn=policy["Arn"],
entity=policy["PolicyId"],
version_id=policy["DefaultVersionId"],
type="Custom" if scope == "Local" else "AWS",
attached=(True if policy["AttachmentCount"] > 0 else False),
)
except Exception as error:
logger.error(
@@ -773,7 +768,7 @@ class IAM(AWSService):
def _list_policies_version(self, policies):
logger.info("IAM - List Policies Version...")
try:
for policy in policies:
for policy in policies.values():
try:
policy_version = self.client.get_policy_version(
PolicyArn=policy.arn, VersionId=policy.version_id
@@ -1019,6 +1014,43 @@ class IAM(AWSService):
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_service_specific_credentials(self):
logger.info("IAM - List Service Specific Credentials...")
try:
for user in self.users:
service_specific_credentials = (
self.client.list_service_specific_credentials(UserName=user.name)
)
for credential in service_specific_credentials.get(
"ServiceSpecificCredentials", []
):
credential["Arn"] = (
f"arn:{self.audited_partition}:iam:{self.region}:{self.audited_account}:user/{user.name}/credential/{credential['ServiceSpecificCredentialId']}"
)
if not self.audit_resources or (
is_resource_filtered(credential["Arn"], self.audit_resources)
):
self.service_specific_credentials.append(
ServiceSpecificCredential(
arn=credential["Arn"],
user=user,
status=credential["Status"],
create_date=credential["CreateDate"],
service_user_name=credential.get("ServiceUserName"),
service_credential_alias=credential.get(
"ServiceCredentialAlias"
),
expiration_date=credential.get("ExpirationDate"),
id=credential.get("ServiceSpecificCredentialId"),
service_name=credential.get("ServiceName"),
region=self.region,
)
)
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class MFADevice(BaseModel):
serial_number: str
@@ -1046,6 +1078,19 @@ class Role(BaseModel):
tags: Optional[list]
class ServiceSpecificCredential(BaseModel):
arn: str
user: User
status: str
create_date: datetime
service_user_name: Optional[str]
service_credential_alias: Optional[str]
expiration_date: Optional[datetime]
id: str
service_name: str
region: str
class Group(BaseModel):
name: str
arn: str
@@ -3,7 +3,7 @@
"CheckID": "ssmincidents_enabled_with_plans",
"CheckTitle": "Ensure SSM Incidents is enabled with response plans.",
"CheckType": [],
"ServiceName": "ssm",
"ServiceName": "ssmincidents",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:ssm:region:account-id:document/document-name",
"Severity": "low",
@@ -3,7 +3,7 @@
"CheckID": "trustedadvisor_premium_support_plan_subscribed",
"CheckTitle": "Check if a Premium support plan is subscribed",
"CheckType": [],
"ServiceName": "support",
"ServiceName": "trustedadvisor",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:iam::AWS_ACCOUNT_NUMBER:root",
"Severity": "low",
@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "defender_attack_path_notifications_properly_configured",
"CheckTitle": "Ensure that email notifications for attack paths are enabled with minimal risk level",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AzureEmailNotifications",
"Description": "Ensure that Microsoft Defender for Cloud is configured to send email notifications for attack paths identified in the Azure subscription with an appropriate minimal risk level.",
"Risk": "If attack path notifications are not enabled, security teams may not be promptly informed about exploitable attack sequences, increasing the risk of delayed mitigation and potential breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable attack path email notifications in Microsoft Defender for Cloud to ensure that security teams are notified when potential attack paths are identified. Configure the minimal risk level as appropriate for your organization.",
"Url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,51 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.defender.defender_client import defender_client
class defender_attack_path_notifications_properly_configured(Check):
"""
Ensure that email notifications for attack paths are enabled.
This check evaluates whether Microsoft Defender for Cloud is configured to send email notifications for attack paths in each Azure subscription.
- PASS: Notifications are enabled for attack paths with a risk level set (not None) and equal or higher than the configured minimum.
- FAIL: Notifications are not enabled for attack paths in the subscription or the risk level is too low.
"""
def execute(self) -> list[Check_Report_Azure]:
findings = []
# Get the minimal risk level from config, default to 'High'
risk_levels = ["Low", "Medium", "High", "Critical"]
min_risk_level = defender_client.audit_config.get(
"defender_attack_path_minimal_risk_level", "High"
)
if min_risk_level not in risk_levels:
min_risk_level = "High"
min_risk_index = risk_levels.index(min_risk_level)
for (
subscription_name,
security_contact_configurations,
) in defender_client.security_contact_configurations.items():
for contact_configuration in security_contact_configurations.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=contact_configuration
)
report.subscription = subscription_name
actual_risk_level = getattr(
contact_configuration, "attack_path_minimal_risk_level", None
)
if not actual_risk_level or actual_risk_level not in risk_levels:
report.status = "FAIL"
report.status_extended = f"Attack path notifications are not enabled in subscription {subscription_name} for security contact {contact_configuration.name}."
else:
actual_risk_index = risk_levels.index(actual_risk_level)
if actual_risk_index <= min_risk_index:
report.status = "PASS"
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
else:
report.status = "FAIL"
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
findings.append(report)
return findings
@@ -3,7 +3,7 @@
"CheckID": "compute_firewall_rdp_access_from_the_internet_allowed",
"CheckTitle": "Ensure That RDP Access Is Restricted From the Internet",
"CheckType": [],
"ServiceName": "networking",
"ServiceName": "compute",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
@@ -3,7 +3,7 @@
"CheckID": "compute_firewall_ssh_access_from_the_internet_allowed",
"CheckTitle": "Ensure That SSH Access Is Restricted From the Internet",
"CheckType": [],
"ServiceName": "networking",
"ServiceName": "compute",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_bind_address",
"CheckTitle": "Ensure that the --bind-address argument is set to 127.0.0.1",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_disable_profiling",
"CheckTitle": "Ensure that the --profiling argument is set to false",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_garbage_collection",
"CheckTitle": "Ensure that the --terminated-pod-gc-threshold argument is set as appropriate",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_root_ca_file_set",
"CheckTitle": "Ensure that the --root-ca-file argument is set as appropriate",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_rotate_kubelet_server_cert",
"CheckTitle": "Ensure that the RotateKubeletServerCertificate argument is set to true",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_service_account_credentials",
"CheckTitle": "Ensure that the --use-service-account-credentials argument is set to true",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "controllermanager_service_account_private_key_file",
"CheckTitle": "Ensure that the --service-account-private-key-file argument is set as appropriate",
"CheckType": [],
"ServiceName": "controller-manager",
"ServiceName": "controllermanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
@@ -3,7 +3,7 @@
"CheckID": "rbac_cluster_admin_usage",
"CheckTitle": "Ensure that the cluster-admin role is only used where required",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_csr_approval_access",
"CheckTitle": "Minimize access to the approval sub-resource of certificatesigningrequests objects",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_node_proxy_subresource_access",
"CheckTitle": "Minimize access to the proxy sub-resource of nodes",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_pod_creation_access",
"CheckTitle": "Minimize access to create pods",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_pv_creation_access",
"CheckTitle": "Minimize access to create persistent volumes",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_secret_access",
"CheckTitle": "Minimize access to secrets",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_service_account_token_creation",
"CheckTitle": "Minimize access to the service account token creation",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_webhook_config_access",
"CheckTitle": "Minimize access to webhook configuration objects",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -3,7 +3,7 @@
"CheckID": "rbac_minimize_wildcard_use_roles",
"CheckTitle": "Minimize wildcard use in Roles and ClusterRoles",
"CheckType": [],
"ServiceName": "RBAC",
"ServiceName": "rbac",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
@@ -7,9 +7,10 @@ class M365Mutelist(Mutelist):
def is_finding_muted(
self,
finding: CheckReportM365,
tenant_id: str,
) -> bool:
return self.is_muted(
finding.tenant_id,
tenant_id,
finding.check_metadata.CheckID,
finding.location,
finding.resource_name,

Some files were not shown because too many files have changed in this diff Show More