Compare commits

...

43 Commits

Author SHA1 Message Date
Alan Buscaglia 2a3c71c435 refactor: simplify gga script and exclude api/ via config 2025-12-16 14:07:42 +01:00
Alan Buscaglia 07a995a210 docs(changelog): add gga integration entry for PR #9571 2025-12-16 14:05:05 +01:00
Alan Buscaglia 73bf93a036 refactor: optimize AGENTS-CODE-REVIEW.md for LLM parsing
- Use REJECT/REQUIRE/PREFER keywords for clear action items
- Remove verbose explanations
- Use bullet points instead of paragraphs
- Add clear section separators
- Condense rules to single lines where possible
2025-12-16 13:52:27 +01:00
Alan Buscaglia d53e76e30a fix: add missing UI rules (shadcn, DRY/KISS, responsive) 2025-12-16 13:51:17 +01:00
Alan Buscaglia e2344b4fc6 fix: skip AI review for directories without AGENTS.md (opt-in model)
Components must have their own AGENTS.md to be included in AI code review.
This allows teams like API to opt-in when ready without blocking their workflow.
2025-12-16 13:49:13 +01:00
Alan Buscaglia eb1fc2b269 fix: add explicit brew tap before installing gga 2025-12-16 13:46:55 +01:00
Alan Buscaglia 4931564648 feat: replace custom Claude validation with Gentleman Guardian Angel (gga)
- Add .gga config in repo root for monorepo support (UI + Python)
- Add AGENTS-CODE-REVIEW.md with centralized code review rules
- Add scripts/gga-review.sh for pre-commit integration with auto-install
- Update .pre-commit-config.yaml to run gga after all formatters/linters
- Simplify ui/.husky/pre-commit to only run healthcheck + build
- Update ui/README.md with new gga documentation

Benefits:
- Provider agnostic: supports Claude, Gemini, Codex, Ollama
- Smart caching: skips unchanged files
- Monorepo support: reviews both TypeScript and Python
- Runs last: reviews code after formatting is applied
- Controlled by CODE_REVIEW_ENABLED environment variable
2025-12-16 13:40:04 +01:00
Rubén De la Torre Vico 433853493b chore(aws): enhance metadata for trustedadvisor service (#9435)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-16 12:49:00 +01:00
Rubén De la Torre Vico 5aa112d438 chore(aws): enhance metadata for sns service (#9428)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-16 12:33:49 +01:00
Rubén De la Torre Vico 1b2c73d2e3 chore(aws): enhance metadata for servicecatalog service (#9410)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-16 12:12:36 +01:00
Rubén De la Torre Vico 90e3fabc33 chore(aws): enhance metadata for inspector2 service (#9260)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-16 11:44:49 +01:00
Daniel Barranquero d4b90abd10 chore(mongodbatlas): store location as lowercase (#9554)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2025-12-16 10:40:49 +01:00
Hugo Pereira Brito 251fc6d4e3 fix: changelog trust-boundaries entry (#9563) 2025-12-16 10:06:38 +01:00
Hugo Pereira Brito dd85da703e chore: update prowler hub docs picture (#9564) 2025-12-16 09:40:27 +01:00
Adrián Peña b549c8dbad fix: make scan_id mandatory in compliance overviews endpoint (#9560) 2025-12-15 17:27:45 +01:00
Víctor Fernández Poyatos 79ac7cf6d4 fix(beat): Increase scheduled scans countdown to 5 seconds (#9558) 2025-12-15 17:13:08 +01:00
Rubén De la Torre Vico d292c6e58a chore(aws): enhance metadata for memorydb service (#9266)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-15 17:11:44 +01:00
Alan Buscaglia 8f361e7e8d feat(ui): add Risk Radar component with API integration (#9532) 2025-12-15 17:02:21 +01:00
Rubén De la Torre Vico 3eb278cb9f chore(aws): enhance metadata for kms service (#9263)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-15 16:56:17 +01:00
Rubén De la Torre Vico 2f7eec8bca chore(aws): enhance metadata for kafka service (#9261)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-15 13:13:47 +01:00
César Arroba 00063c57de chore(github): fix container checks workflows (#9556) 2025-12-15 13:06:18 +01:00
César Arroba 2341b5bc7d chore(github): check containers workflow only for prowler (#9555) 2025-12-15 12:47:36 +01:00
Rubén De la Torre Vico 4015beff20 docs(mcp_server): update documentation and add developer guide for extensibility (#9533) 2025-12-15 12:35:59 +01:00
Rubén De la Torre Vico ab475bafc3 chore(aws): enhance metadata for glue service (#9258)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-15 12:07:11 +01:00
Andoni Alonso b4ce01afd4 feat(iac): set only misconfig and secret as default scanners (#9553) 2025-12-15 12:01:31 +01:00
Chandrapal Badshah 2b4b23c719 feat(lighthouse): filter out non-compatible OpenAI models (#9523)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-12-15 11:31:04 +01:00
César Arroba 4398b00801 chore(github): use QEMU to build ARM images if repository is not prowler (#9547) 2025-12-15 11:23:39 +01:00
Rubén De la Torre Vico e0cf8bffd4 feat(mcp_server): update API base URL environment variable to include complete path (#9542) 2025-12-15 11:04:44 +01:00
Daniel Barranquero 6761f0ffd0 docs: add mongodbatlas app support (#9312) 2025-12-15 10:57:27 +01:00
Hugo Pereira Brito 51bbaeb403 fix: trustboundaries category typo to trust-boundaries (#9536) 2025-12-15 10:48:33 +01:00
Pepe Fagoaga 6158c16108 feat(categories): add privilege-escalation and ec2-imdsv1 (#9537) 2025-12-12 15:14:26 +01:00
Alejandro Bailo 0c2c5ea265 chore: update React 19.2.2 for security improvements (#9534) 2025-12-12 14:11:01 +01:00
bota4go 3b56166c34 fix(apigateway): retrieve correct logingLevel status (#9304)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-12-12 13:44:37 +01:00
Víctor Fernández Poyatos b5151a8ee5 feat(api): new endpoint for categories overviews (#9529) 2025-12-12 13:30:59 +01:00
Alejandro Bailo 0495267351 feat: resource details added to findigns and resource view (#9515) 2025-12-12 13:12:17 +01:00
Pepe Fagoaga eefe045c18 docs(security): add more details (#9525)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2025-12-12 11:03:12 +01:00
Alejandro Bailo d7d1b22c45 chore(dependencies): update @next/third-parties to version 15.5.7 (#9513) 2025-12-12 11:00:48 +01:00
dependabot[bot] 439dbe679b build(deps): bump next from 15.5.7 to 15.5.9 in /ui (#9522)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-12-12 10:17:34 +01:00
Adrián Peña 0e9ba4b116 fix(api): add one second countdown to scheduled scan task to ensure transaction completion (#9516) 2025-12-12 10:08:42 +01:00
Pepe Fagoaga 89295f7e7d chore(overview): adjust wording for Prowler ThreatScore (#9524) 2025-12-12 09:18:58 +01:00
StylusFrost 7cf7758851 docs(k8s): enhance token management guidance in getting started guide (#9519) 2025-12-12 08:37:33 +01:00
Pepe Fagoaga 06142094cd chore(readme): Add LFX health score badge (#9297) 2025-12-11 19:34:40 +01:00
Prowler Bot 93f1c02f44 chore(release): Bump version to v5.16.0 (#9520)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-12-11 17:23:45 +01:00
198 changed files with 6882 additions and 3928 deletions
+20
View File
@@ -0,0 +1,20 @@
# Gentleman Guardian Angel (gga) Configuration
# https://github.com/Gentleman-Programming/gentleman-guardian-angel
# AI Provider (required)
# Options: claude, gemini, codex, ollama:<model>
PROVIDER="claude"
# File patterns to include in review (comma-separated globs)
# Review both TypeScript (UI) and Python (SDK, API, MCP) files
FILE_PATTERNS="*.ts,*.tsx,*.js,*.jsx,*.py"
# File patterns to exclude from review (comma-separated globs)
# Excludes: test files, type definitions, and api/ folder (no AGENTS.md yet)
EXCLUDE_PATTERNS="*.test.ts,*.test.tsx,*.spec.ts,*.spec.tsx,*.d.ts,*_test.py,test_*.py,conftest.py,api/*"
# File containing your coding standards (relative to repo root)
RULES_FILE="AGENTS-CODE-REVIEW.md"
# Strict mode: fail if AI response is ambiguous (recommended)
STRICT_MODE="true"
+3 -1
View File
@@ -20,6 +20,7 @@ env:
jobs:
api-dockerfile-lint:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
@@ -43,6 +44,7 @@ jobs:
ignore: DL3013
api-container-build-and-scan:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ${{ matrix.runner }}
strategy:
matrix:
@@ -90,7 +92,7 @@ jobs:
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Scan container with Trivy for ${{ matrix.arch }}
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/trivy-scan
with:
image-name: ${{ env.IMAGE_NAME }}
+3 -1
View File
@@ -20,6 +20,7 @@ env:
jobs:
mcp-dockerfile-lint:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
@@ -42,6 +43,7 @@ jobs:
dockerfile: mcp_server/Dockerfile
mcp-container-build-and-scan:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ${{ matrix.runner }}
strategy:
matrix:
@@ -88,7 +90,7 @@ jobs:
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Scan MCP container with Trivy for ${{ matrix.arch }}
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/trivy-scan
with:
image-name: ${{ env.IMAGE_NAME }}
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Scan SDK container with Trivy for ${{ matrix.arch }}
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/trivy-scan
with:
image-name: ${{ env.IMAGE_NAME }}
+3 -1
View File
@@ -20,6 +20,7 @@ env:
jobs:
ui-dockerfile-lint:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
@@ -43,6 +44,7 @@ jobs:
ignore: DL3018
ui-container-build-and-scan:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ${{ matrix.runner }}
strategy:
matrix:
@@ -92,7 +94,7 @@ jobs:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LwpXXXX
- name: Scan UI container with Trivy for ${{ matrix.arch }}
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/trivy-scan
with:
image-name: ${{ env.IMAGE_NAME }}
+12 -2
View File
@@ -82,7 +82,6 @@ repos:
args: ["--directory=./"]
pass_filenames: false
- repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta
hooks:
@@ -129,9 +128,20 @@ repos:
- id: ui-checks
name: UI - Husky Pre-commit
description: "Run UI pre-commit checks (Claude Code validation + healthcheck)"
description: "Run UI pre-commit checks (healthcheck + build)"
entry: bash -c 'cd ui && .husky/pre-commit'
language: system
files: '^ui/.*\.(ts|tsx|js|jsx|json|css)$'
pass_filenames: false
verbose: true
- id: gga
name: Gentleman Guardian Angel (AI Code Review)
description: "AI-powered code review - runs last after all formatters/linters"
entry: ./scripts/gga-review.sh
language: system
files: '\.(ts|tsx|js|jsx|py)$'
exclude: '(\.test\.|\.spec\.|_test\.py|test_.*\.py|conftest\.py|\.d\.ts)'
pass_filenames: false
stages: ["pre-commit"]
verbose: true
+89
View File
@@ -0,0 +1,89 @@
# Code Review Rules
## References
- UI details: `ui/AGENTS.md`
- SDK details: `prowler/AGENTS.md`
- MCP details: `mcp_server/AGENTS.md`
---
## ALL FILES
REJECT if:
- Hardcoded secrets/credentials
- `any` type (TypeScript) or missing type hints (Python)
- Code duplication (violates DRY)
- Silent error handling (no logging)
---
## TypeScript/React (ui/)
REJECT if:
- `import React` or `import * as React` → use `import { useState }`
- Union types `type X = "a" | "b"` → use `const X = {...} as const`
- `var()` or hex colors in className → use Tailwind classes
- `useMemo` or `useCallback` without justification (React 19 Compiler)
- `z.string().email()` → use `z.email()` (Zod v4)
- `z.string().nonempty()` → use `z.string().min(1)` (Zod v4)
- Missing `"use client"` in client components
- Missing `"use server"` in server actions
- Images without `alt` attribute
- Interactive elements without `aria` labels
- Non-semantic HTML when semantic exists
PREFER:
- `components/shadcn/` over custom components
- `cn()` for conditional/merged classes
- Local files if used 1 place, `components/shared/` if 2+
- Responsive classes: `sm:`, `md:`, `lg:`, `xl:`
EXCEPTION:
- `var()` allowed in chart/graph component props (not className)
---
## Python (prowler/, mcp_server/)
REJECT if:
- Missing type hints on public functions
- Missing docstrings on classes/public methods
- Bare `except:` without specific exception
- `print()` instead of `logger`
REQUIRE for SDK checks:
- Inherit from `Check`
- `execute()` returns `list[CheckReport]`
- `report.status` = `"PASS"` | `"FAIL"`
- `.metadata.json` file exists
REQUIRE for MCP tools:
- Extend `BaseTool` (auto-registration)
- `MinimalSerializerMixin` for responses
- `from_api_response()` for API transforms
---
## Response Format
FIRST LINE must be exactly:
```
STATUS: PASSED
```
or
```
STATUS: FAILED
```
If FAILED, list: `file:line - rule - issue`
+1
View File
@@ -23,6 +23,7 @@
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/toniblyx/prowler"></a>
<a href="https://gallery.ecr.aws/prowler-cloud/prowler"><img width="120" height=19" alt="AWS ECR Gallery" src="https://user-images.githubusercontent.com/3985464/151531396-b6535a68-c907-44eb-95a1-a09508178616.png"></a>
<a href="https://codecov.io/gh/prowler-cloud/prowler"><img src="https://codecov.io/gh/prowler-cloud/prowler/graph/badge.svg?token=OflBGsdpDl"/></a>
<a href="https://insights.linuxfoundation.org/project/prowler-cloud-prowler"><img src="https://insights.linuxfoundation.org/api/badge/health-score?project=prowler-cloud-prowler"/></a>
</p>
<p align="center">
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
+21 -1
View File
@@ -2,6 +2,27 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.17.0] (Prowler UNRELEASED)
### Added
- New endpoint to retrieve and overview of the categories based on finding severities [(#9529)](https://github.com/prowler-cloud/prowler/pull/9529)
- Endpoints `GET /findings` and `GET /findings/latests` can now use the category filter [(#9529)](https://github.com/prowler-cloud/prowler/pull/9529)
### Changed
- Endpoint `GET /overviews/attack-surfaces` no longer returns the related check IDs [(#9529)](https://github.com/prowler-cloud/prowler/pull/9529)
- OpenAI provider to only load chat-compatible models with tool calling support [(#9523)](https://github.com/prowler-cloud/prowler/pull/9523)
- Increased execution delay for the first scheduled scan tasks to 5 seconds[(#9558)](https://github.com/prowler-cloud/prowler/pull/9558)
### Fixed
- Make `scan_id` a required filter in the compliance overview endpoint [(#9560)](https://github.com/prowler-cloud/prowler/pull/9560)
---
## [1.16.1] (Prowler v5.15.1)
### Fixed
- Race condition in scheduled scan creation by adding countdown to task [(#9516)](https://github.com/prowler-cloud/prowler/pull/9516)
## [1.16.0] (Prowler v5.15.0)
### Added
@@ -12,7 +33,6 @@ All notable changes to the **Prowler API** are documented in this file.
- Support to use admin credentials through the read replica database [(#9440)](https://github.com/prowler-cloud/prowler/pull/9440)
### Changed
- Error messages from Lighthouse celery tasks [(#9165)](https://github.com/prowler-cloud/prowler/pull/9165)
- Restore the compliance overview endpoint's mandatory filters [(#9338)](https://github.com/prowler-cloud/prowler/pull/9338)
-11
View File
@@ -40,7 +40,6 @@ class ApiConfig(AppConfig):
self._ensure_crypto_keys()
load_prowler_compliance()
self._initialize_attack_surface_mapping()
def _ensure_crypto_keys(self):
"""
@@ -168,13 +167,3 @@ class ApiConfig(AppConfig):
f"Error generating JWT keys: {e}. Please set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' manually."
)
raise e
def _initialize_attack_surface_mapping(self):
from tasks.jobs.scan import ( # noqa: F401
_get_attack_surface_mapping_from_provider,
)
from api.models import Provider # noqa: F401
for provider_type, _label in Provider.ProviderChoices.choices:
_get_attack_surface_mapping_from_provider(provider_type)
+27 -1
View File
@@ -43,6 +43,7 @@ from api.models import (
ResourceTag,
Role,
Scan,
ScanCategorySummary,
ScanSummary,
SeverityChoices,
StateChoices,
@@ -157,6 +158,9 @@ class CommonFindingFilters(FilterSet):
field_name="resources__type", lookup_expr="icontains"
)
category = CharFilter(method="filter_category")
category__in = CharInFilter(field_name="categories", lookup_expr="overlap")
# Temporarily disabled until we implement tag filtering in the UI
# resource_tag_key = CharFilter(field_name="resources__tags__key")
# resource_tag_key__in = CharInFilter(
@@ -188,6 +192,9 @@ class CommonFindingFilters(FilterSet):
def filter_resource_type(self, queryset, name, value):
return queryset.filter(resource_types__contains=[value])
def filter_category(self, queryset, name, value):
return queryset.filter(categories__contains=[value])
def filter_resource_tag(self, queryset, name, value):
overall_query = Q()
for key_value_pair in value:
@@ -762,7 +769,7 @@ class RoleFilter(FilterSet):
class ComplianceOverviewFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
scan_id = UUIDFilter(field_name="scan_id")
scan_id = UUIDFilter(field_name="scan_id", required=True)
region = CharFilter(field_name="region")
class Meta:
@@ -1096,3 +1103,22 @@ class AttackSurfaceOverviewFilter(FilterSet):
class Meta:
model = AttackSurfaceOverview
fields = {}
class CategoryOverviewFilter(FilterSet):
provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
provider_type = ChoiceFilter(
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
)
provider_type__in = ChoiceInFilter(
field_name="scan__provider__provider",
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
category = CharFilter(field_name="category", lookup_expr="exact")
category__in = CharInFilter(field_name="category", lookup_expr="in")
class Meta:
model = ScanCategorySummary
fields = {}
@@ -0,0 +1,108 @@
import uuid
import django.db.models.deletion
from django.db import migrations, models
import api.db_utils
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0062_backfill_daily_severity_summaries"),
]
operations = [
migrations.CreateModel(
name="ScanCategorySummary",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"tenant_id",
models.UUIDField(db_index=True, editable=False),
),
(
"inserted_at",
models.DateTimeField(auto_now_add=True),
),
(
"scan",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="category_summaries",
related_query_name="category_summary",
to="api.scan",
),
),
(
"category",
models.CharField(max_length=100),
),
(
"severity",
api.db_utils.SeverityEnumField(
choices=[
("critical", "Critical"),
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("informational", "Informational"),
],
max_length=50,
),
),
(
"total_findings",
models.IntegerField(
default=0, help_text="Non-muted findings (PASS + FAIL)"
),
),
(
"failed_findings",
models.IntegerField(
default=0,
help_text="Non-muted FAIL findings (subset of total_findings)",
),
),
(
"new_failed_findings",
models.IntegerField(
default=0,
help_text="Non-muted FAIL with delta='new' (subset of failed_findings)",
),
),
],
options={
"db_table": "scan_category_summaries",
},
),
migrations.AddIndex(
model_name="scancategorysummary",
index=models.Index(
fields=["tenant_id", "scan"], name="scs_tenant_scan_idx"
),
),
migrations.AddConstraint(
model_name="scancategorysummary",
constraint=models.UniqueConstraint(
fields=("tenant_id", "scan_id", "category", "severity"),
name="unique_category_severity_per_scan",
),
),
migrations.AddConstraint(
model_name="scancategorysummary",
constraint=api.rls.RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_scancategorysummary",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
@@ -0,0 +1,21 @@
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0063_scan_category_summary"),
]
operations = [
migrations.AddField(
model_name="finding",
name="categories",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=100),
blank=True,
null=True,
size=None,
),
),
]
+66
View File
@@ -868,6 +868,14 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
null=True,
)
# Check metadata denormalization
categories = ArrayField(
models.CharField(max_length=100),
blank=True,
null=True,
help_text="Categories from check metadata for efficient filtering",
)
# Relationships
scan = models.ForeignKey(to=Scan, related_name="findings", on_delete=models.CASCADE)
@@ -1951,6 +1959,64 @@ class ResourceScanSummary(RowLevelSecurityProtectedModel):
]
class ScanCategorySummary(RowLevelSecurityProtectedModel):
"""
Pre-aggregated category metrics per scan by severity.
Stores one row per (category, severity) combination per scan for efficient
overview queries. Categories come from check_metadata.categories.
Count relationships (each is a subset of the previous):
- total_findings >= failed_findings >= new_failed_findings
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
scan = models.ForeignKey(
Scan,
on_delete=models.CASCADE,
related_name="category_summaries",
related_query_name="category_summary",
)
category = models.CharField(max_length=100)
severity = SeverityEnumField(choices=SeverityChoices)
total_findings = models.IntegerField(
default=0, help_text="Non-muted findings (PASS + FAIL)"
)
failed_findings = models.IntegerField(
default=0, help_text="Non-muted FAIL findings (subset of total_findings)"
)
new_failed_findings = models.IntegerField(
default=0,
help_text="Non-muted FAIL with delta='new' (subset of failed_findings)",
)
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "scan_category_summaries"
indexes = [
models.Index(fields=["tenant_id", "scan"], name="scs_tenant_scan_idx"),
]
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "scan_id", "category", "severity"),
name="unique_category_severity_per_scan",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
class JSONAPIMeta:
resource_name = "scan-category-summaries"
class LighthouseConfiguration(RowLevelSecurityProtectedModel):
"""
Stores configuration and API keys for LLM services.
+392 -25
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.16.0
version: 1.17.0
description: |-
Prowler API specification.
@@ -711,6 +711,7 @@ paths:
- severity
- check_id
- check_metadata
- categories
- raw_result
- inserted_at
- updated_at
@@ -723,6 +724,19 @@ paths:
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[category]
schema:
type: string
- in: query
name: filter[category__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[check_id]
schema:
@@ -1205,6 +1219,7 @@ paths:
- severity
- check_id
- check_metadata
- categories
- raw_result
- inserted_at
- updated_at
@@ -1265,6 +1280,19 @@ paths:
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[category]
schema:
type: string
- in: query
name: filter[category__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[check_id]
schema:
@@ -1722,6 +1750,7 @@ paths:
- severity
- check_id
- check_metadata
- categories
- raw_result
- inserted_at
- updated_at
@@ -1734,6 +1763,19 @@ paths:
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[category]
schema:
type: string
- in: query
name: filter[category__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[check_id]
schema:
@@ -2151,9 +2193,23 @@ paths:
- services
- regions
- resource_types
- categories
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[category]
schema:
type: string
- in: query
name: filter[category__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[check_id]
schema:
@@ -2609,9 +2665,23 @@ paths:
- services
- regions
- resource_types
- categories
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[category]
schema:
type: string
- in: query
name: filter[category__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[check_id]
schema:
@@ -4499,7 +4569,7 @@ paths:
description: No response body
/api/v1/overviews/attack-surfaces:
get:
operationId: overviews_attack_surfaces_retrieve
operationId: overviews_attack_surfaces_list
description: Retrieve aggregated attack surface metrics from latest completed
scans per provider.
summary: Get attack surface overview
@@ -4515,31 +4585,116 @@ paths:
- total_findings
- failed_findings
- muted_failed_findings
- check_ids
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[provider_id.in]
schema:
type: string
description: Filter by multiple provider IDs (comma-separated UUIDs)
- in: query
name: filter[provider_id]
schema:
type: string
format: uuid
description: Filter by specific provider ID
- in: query
name: filter[provider_type.in]
name: filter[provider_id__in]
schema:
type: string
description: Filter by multiple provider types (comma-separated)
type: array
items:
type: string
format: uuid
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_type]
schema:
type: string
description: Filter by provider type (aws, azure, gcp, etc.)
x-spec-enum-id: eca8c51e6bd28935
enum:
- aws
- azure
- gcp
- github
- iac
- kubernetes
- m365
- mongodbatlas
- oraclecloud
description: |-
* `aws` - AWS
* `azure` - Azure
* `gcp` - GCP
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
schema:
type: array
items:
type: string
x-spec-enum-id: eca8c51e6bd28935
enum:
- aws
- azure
- gcp
- github
- iac
- kubernetes
- m365
- mongodbatlas
- oraclecloud
description: |-
Multiple values may be separated by commas.
* `aws` - AWS
* `azure` - Azure
* `gcp` - GCP
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
style: form
- name: filter[search]
required: false
in: query
description: A search term.
schema:
type: string
- name: page[number]
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page[size]
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: sort
required: false
in: query
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
schema:
type: array
items:
type: string
enum:
- id
- -id
- total_findings
- -total_findings
- failed_findings
- -failed_findings
- muted_failed_findings
- -muted_failed_findings
explode: false
tags:
- Overview
security:
@@ -4549,7 +4704,164 @@ paths:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/AttackSurfaceOverviewResponse'
$ref: '#/components/schemas/PaginatedAttackSurfaceOverviewList'
description: ''
/api/v1/overviews/categories:
get:
operationId: overviews_categories_list
description: 'Retrieve aggregated category metrics from latest completed scans
per provider. Returns one row per category with total, failed, and new failed
findings counts, plus a severity breakdown showing failed findings per severity
level. '
summary: Get category overview
parameters:
- in: query
name: fields[category-overviews]
schema:
type: array
items:
type: string
enum:
- id
- total_findings
- failed_findings
- new_failed_findings
- severity
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[category]
schema:
type: string
- in: query
name: filter[category__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_id]
schema:
type: string
format: uuid
- in: query
name: filter[provider_id__in]
schema:
type: array
items:
type: string
format: uuid
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: eca8c51e6bd28935
enum:
- aws
- azure
- gcp
- github
- iac
- kubernetes
- m365
- mongodbatlas
- oraclecloud
description: |-
* `aws` - AWS
* `azure` - Azure
* `gcp` - GCP
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
schema:
type: array
items:
type: string
x-spec-enum-id: eca8c51e6bd28935
enum:
- aws
- azure
- gcp
- github
- iac
- kubernetes
- m365
- mongodbatlas
- oraclecloud
description: |-
Multiple values may be separated by commas.
* `aws` - AWS
* `azure` - Azure
* `gcp` - GCP
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
style: form
- name: filter[search]
required: false
in: query
description: A search term.
schema:
type: string
- name: page[number]
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page[size]
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: sort
required: false
in: query
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
schema:
type: array
items:
type: string
enum:
- id
- -id
- total_findings
- -total_findings
- failed_findings
- -failed_findings
- new_failed_findings
- -new_failed_findings
- severity
- -severity
explode: false
tags:
- Overview
security:
- JWT or API Key: []
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/PaginatedCategoryOverviewList'
description: ''
/api/v1/overviews/findings:
get:
@@ -4951,7 +5263,7 @@ paths:
summary: Get findings severity data over time
parameters:
- in: query
name: fields[findings-severity-timeseries]
name: fields[findings-severity-over-time]
schema:
type: array
items:
@@ -4972,10 +5284,12 @@ paths:
name: filter[date_from]
schema:
type: string
format: date
- in: query
name: filter[date_to]
schema:
type: string
format: date
- in: query
name: filter[provider_id]
schema:
@@ -10846,23 +11160,46 @@ components:
type: integer
muted_failed_findings:
type: integer
check_ids:
type: array
items:
type: string
readOnly: true
required:
- id
- total_findings
- failed_findings
- muted_failed_findings
AttackSurfaceOverviewResponse:
CategoryOverview:
type: object
properties:
data:
$ref: '#/components/schemas/AttackSurfaceOverview'
required:
- data
- type
- id
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- category-overviews
id: {}
attributes:
type: object
properties:
id:
type: string
total_findings:
type: integer
failed_findings:
type: integer
new_failed_findings:
type: integer
severity:
description: 'Severity breakdown: {informational, low, medium, high,
critical}'
required:
- id
- total_findings
- failed_findings
- new_failed_findings
- severity
ComplianceOverview:
type: object
required:
@@ -11079,6 +11416,13 @@ components:
type: string
maxLength: 100
check_metadata: {}
categories:
type: array
items:
type: string
maxLength: 100
nullable: true
description: Categories from check metadata for efficient filtering
raw_result: {}
inserted_at:
type: string
@@ -11229,10 +11573,15 @@ components:
type: array
items:
type: string
categories:
type: array
items:
type: string
required:
- services
- regions
- resource_types
- categories
FindingMetadataResponse:
type: object
properties:
@@ -13935,6 +14284,24 @@ components:
$ref: '#/components/schemas/OverviewSeverity'
required:
- data
PaginatedAttackSurfaceOverviewList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/AttackSurfaceOverview'
required:
- data
PaginatedCategoryOverviewList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/CategoryOverview'
required:
- data
PaginatedComplianceOverviewAttributesList:
type: object
properties:
+286 -38
View File
@@ -4267,6 +4267,74 @@ class TestFindingViewSet:
assert attributes["regions"] == latest_scan_finding.resource_regions
assert attributes["resource_types"] == latest_scan_finding.resource_types
def test_findings_metadata_categories(
self, authenticated_client, findings_with_categories
):
finding = findings_with_categories
response = authenticated_client.get(
reverse("finding-metadata"),
{"filter[inserted_at]": finding.inserted_at.strftime("%Y-%m-%d")},
)
assert response.status_code == status.HTTP_200_OK
attributes = response.json()["data"]["attributes"]
assert set(attributes["categories"]) == {"gen-ai", "security"}
def test_findings_metadata_latest_categories(
self, authenticated_client, latest_scan_finding_with_categories
):
response = authenticated_client.get(
reverse("finding-metadata_latest"),
)
assert response.status_code == status.HTTP_200_OK
attributes = response.json()["data"]["attributes"]
assert set(attributes["categories"]) == {"gen-ai", "iam"}
def test_findings_filter_by_category(
self, authenticated_client, findings_with_categories
):
finding = findings_with_categories
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[category]": "gen-ai",
"filter[inserted_at]": finding.inserted_at.strftime("%Y-%m-%d"),
},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 1
assert set(response.json()["data"][0]["attributes"]["categories"]) == {
"gen-ai",
"security",
}
def test_findings_filter_by_category_in(
self, authenticated_client, findings_with_multiple_categories
):
finding1, _ = findings_with_multiple_categories
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[category__in]": "gen-ai,iam",
"filter[inserted_at]": finding1.inserted_at.strftime("%Y-%m-%d"),
},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
def test_findings_filter_by_category_no_match(
self, authenticated_client, findings_with_categories
):
finding = findings_with_categories
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[category]": "nonexistent",
"filter[inserted_at]": finding.inserted_at.strftime("%Y-%m-%d"),
},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
@pytest.mark.django_db
class TestJWTFields:
@@ -7258,7 +7326,6 @@ class TestOverviewViewSet:
assert item["attributes"]["total_findings"] == 0
assert item["attributes"]["failed_findings"] == 0
assert item["attributes"]["muted_failed_findings"] == 0
assert item["attributes"]["check_ids"] == []
def test_overview_attack_surface_with_data(
self,
@@ -7270,13 +7337,6 @@ class TestOverviewViewSet:
tenant = tenants_fixture[0]
provider = providers_fixture[0]
mapping = {
"internet-exposed": {"aws-check-1", "aws-check-2"},
"secrets": {"aws-secret-check"},
"privilege-escalation": {"aws-priv-check"},
"ec2-imdsv1": {"aws-imdsv1-check"},
}
scan = Scan.objects.create(
name="attack-surface-scan",
provider=provider,
@@ -7302,11 +7362,7 @@ class TestOverviewViewSet:
muted_failed=2,
)
with patch(
"api.v1.views._get_attack_surface_mapping_from_provider",
return_value=mapping,
):
response = authenticated_client.get(reverse("overview-attack-surface"))
response = authenticated_client.get(reverse("overview-attack-surface"))
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 4
@@ -7314,19 +7370,10 @@ class TestOverviewViewSet:
results_by_type = {item["id"]: item["attributes"] for item in data}
assert results_by_type["internet-exposed"]["total_findings"] == 20
assert results_by_type["internet-exposed"]["failed_findings"] == 10
assert set(results_by_type["internet-exposed"]["check_ids"]) == {
"aws-check-1",
"aws-check-2",
}
assert results_by_type["secrets"]["total_findings"] == 15
assert results_by_type["secrets"]["failed_findings"] == 8
assert set(results_by_type["secrets"]["check_ids"]) == {"aws-secret-check"}
assert results_by_type["privilege-escalation"]["total_findings"] == 0
assert set(results_by_type["privilege-escalation"]["check_ids"]) == {
"aws-priv-check"
}
assert results_by_type["ec2-imdsv1"]["total_findings"] == 0
assert set(results_by_type["ec2-imdsv1"]["check_ids"]) == {"aws-imdsv1-check"}
def test_overview_attack_surface_provider_filter(
self,
@@ -7353,13 +7400,6 @@ class TestOverviewViewSet:
tenant=tenant,
)
mapping = {
"internet-exposed": {"shared-check", "shared-check"},
"secrets": set(),
"privilege-escalation": {"priv-check"},
"ec2-imdsv1": {"imdsv1-check"},
}
create_attack_surface_overview(
tenant,
scan1,
@@ -7377,20 +7417,15 @@ class TestOverviewViewSet:
muted_failed=3,
)
with patch(
"api.v1.views._get_attack_surface_mapping_from_provider",
return_value=mapping,
):
response = authenticated_client.get(
reverse("overview-attack-surface"),
{"filter[provider_id]": str(provider1.id)},
)
response = authenticated_client.get(
reverse("overview-attack-surface"),
{"filter[provider_id]": str(provider1.id)},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
results_by_type = {item["id"]: item["attributes"] for item in data}
assert results_by_type["internet-exposed"]["total_findings"] == 10
assert results_by_type["internet-exposed"]["failed_findings"] == 5
assert results_by_type["internet-exposed"]["check_ids"] == ["shared-check"]
def test_overview_services_region_filter(
self, authenticated_client, scan_summaries_fixture
@@ -7633,6 +7668,219 @@ class TestOverviewViewSet:
assert len(data) == 1
assert data[0]["attributes"]["overall_score"] == "80.00"
def test_overview_categories_no_data(self, authenticated_client):
response = authenticated_client.get(reverse("overview-categories"))
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"] == []
def test_overview_categories_aggregates_by_category_with_severity(
self,
authenticated_client,
tenants_fixture,
providers_fixture,
create_scan_category_summary,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan = Scan.objects.create(
name="categories-scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
create_scan_category_summary(
tenant,
scan,
"iam",
"high",
total_findings=20,
failed_findings=10,
new_failed_findings=5,
)
create_scan_category_summary(
tenant,
scan,
"iam",
"medium",
total_findings=15,
failed_findings=8,
new_failed_findings=3,
)
create_scan_category_summary(
tenant,
scan,
"encryption",
"critical",
total_findings=5,
failed_findings=2,
new_failed_findings=1,
)
response = authenticated_client.get(reverse("overview-categories"))
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 2
results_by_category = {item["id"]: item["attributes"] for item in data}
assert results_by_category["iam"]["total_findings"] == 35
assert results_by_category["iam"]["failed_findings"] == 18
assert results_by_category["iam"]["new_failed_findings"] == 8
assert results_by_category["iam"]["severity"]["high"] == 10
assert results_by_category["iam"]["severity"]["medium"] == 8
assert results_by_category["iam"]["severity"]["critical"] == 0
assert results_by_category["encryption"]["total_findings"] == 5
assert results_by_category["encryption"]["failed_findings"] == 2
assert results_by_category["encryption"]["severity"]["critical"] == 2
@pytest.mark.parametrize(
"filter_key,filter_value_fn,expected_total,expected_failed",
[
("filter[provider_id]", lambda p1, _: str(p1.id), 10, 5),
("filter[provider_type]", lambda *_: "aws", 10, 5),
("filter[provider_type__in]", lambda *_: "aws,gcp", 30, 20),
],
)
def test_overview_categories_filters(
self,
authenticated_client,
tenants_fixture,
providers_fixture,
create_scan_category_summary,
filter_key,
filter_value_fn,
expected_total,
expected_failed,
):
tenant = tenants_fixture[0]
provider1, _, gcp_provider, *_ = providers_fixture
scan1 = Scan.objects.create(
name="categories-scan-1",
provider=provider1,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
scan2 = Scan.objects.create(
name="categories-scan-2",
provider=gcp_provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
create_scan_category_summary(
tenant, scan1, "iam", "high", total_findings=10, failed_findings=5
)
create_scan_category_summary(
tenant, scan2, "iam", "high", total_findings=20, failed_findings=15
)
response = authenticated_client.get(
reverse("overview-categories"),
{filter_key: filter_value_fn(provider1, gcp_provider)},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
assert data[0]["attributes"]["total_findings"] == expected_total
assert data[0]["attributes"]["failed_findings"] == expected_failed
def test_overview_categories_category_filter(
self,
authenticated_client,
tenants_fixture,
providers_fixture,
create_scan_category_summary,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan = Scan.objects.create(
name="category-filter-scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
create_scan_category_summary(
tenant, scan, "iam", "high", total_findings=10, failed_findings=5
)
create_scan_category_summary(
tenant, scan, "encryption", "medium", total_findings=20, failed_findings=8
)
create_scan_category_summary(
tenant, scan, "logging", "low", total_findings=15, failed_findings=3
)
response = authenticated_client.get(
reverse("overview-categories"),
{"filter[category__in]": "iam,encryption"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
category_ids = {item["id"] for item in data}
assert category_ids == {"iam", "encryption"}
def test_overview_categories_aggregates_multiple_providers(
self,
authenticated_client,
tenants_fixture,
providers_fixture,
create_scan_category_summary,
):
tenant = tenants_fixture[0]
provider1, provider2, *_ = providers_fixture
scan1 = Scan.objects.create(
name="multi-provider-scan-1",
provider=provider1,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
scan2 = Scan.objects.create(
name="multi-provider-scan-2",
provider=provider2,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
create_scan_category_summary(
tenant,
scan1,
"iam",
"high",
total_findings=10,
failed_findings=5,
new_failed_findings=2,
)
create_scan_category_summary(
tenant,
scan2,
"iam",
"high",
total_findings=15,
failed_findings=8,
new_failed_findings=3,
)
response = authenticated_client.get(reverse("overview-categories"))
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
assert data[0]["id"] == "iam"
assert data[0]["attributes"]["total_findings"] == 25
assert data[0]["attributes"]["failed_findings"] == 13
assert data[0]["attributes"]["new_failed_findings"] == 5
@pytest.mark.django_db
class TestScheduleViewSet:
+8
View File
@@ -382,10 +382,18 @@ def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
regions = sorted({region for region in aggregation["regions"] or [] if region})
resource_types = sorted(set(aggregation["resource_types"] or []))
# Aggregate categories from findings
categories_set = set()
for categories_list in filtered_queryset.values_list("categories", flat=True):
if categories_list:
categories_set.update(categories_list)
categories = sorted(categories_set)
result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
"categories": categories,
}
serializer = FindingMetadataSerializer(data=result)
+17 -3
View File
@@ -1301,6 +1301,7 @@ class FindingSerializer(RLSSerializer):
"severity",
"check_id",
"check_metadata",
"categories",
"raw_result",
"inserted_at",
"updated_at",
@@ -1356,6 +1357,7 @@ class FindingMetadataSerializer(BaseSerializerV1):
resource_types = serializers.ListField(
child=serializers.CharField(), allow_empty=True
)
categories = serializers.ListField(child=serializers.CharField(), allow_empty=True)
# Temporarily disabled until we implement tag filtering in the UI
# tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")
@@ -2242,14 +2244,26 @@ class AttackSurfaceOverviewSerializer(BaseSerializerV1):
total_findings = serializers.IntegerField()
failed_findings = serializers.IntegerField()
muted_failed_findings = serializers.IntegerField()
check_ids = serializers.ListField(
child=serializers.CharField(), allow_empty=True, default=list, read_only=True
)
class JSONAPIMeta:
resource_name = "attack-surface-overviews"
class CategoryOverviewSerializer(BaseSerializerV1):
"""Serializer for category overview aggregations."""
id = serializers.CharField(source="category")
total_findings = serializers.IntegerField()
failed_findings = serializers.IntegerField()
new_failed_findings = serializers.IntegerField()
severity = serializers.JSONField(
help_text="Severity breakdown: {informational, low, medium, high, critical}"
)
class JSONAPIMeta:
resource_name = "category-overviews"
class OverviewRegionSerializer(serializers.Serializer):
id = serializers.SerializerMethodField()
provider_type = serializers.CharField()
+170 -62
View File
@@ -74,7 +74,6 @@ from rest_framework_json_api.views import RelationshipView, Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from tasks.beat import schedule_provider_scan
from tasks.jobs.export import get_s3_client
from tasks.jobs.scan import _get_attack_surface_mapping_from_provider
from tasks.tasks import (
backfill_compliance_summaries_task,
backfill_scan_resource_summaries_task,
@@ -100,6 +99,7 @@ from api.db_utils import rls_transaction
from api.exceptions import TaskFailedException
from api.filters import (
AttackSurfaceOverviewFilter,
CategoryOverviewFilter,
ComplianceOverviewFilter,
CustomDjangoFilterBackend,
DailySeveritySummaryFilter,
@@ -157,6 +157,7 @@ from api.models import (
SAMLDomainIndex,
SAMLToken,
Scan,
ScanCategorySummary,
ScanSummary,
SeverityChoices,
StateChoices,
@@ -178,6 +179,7 @@ from api.uuid_utils import datetime_to_uuid7, uuid7_start
from api.v1.mixins import DisablePaginationMixin, PaginateByPkMixin, TaskManagementMixin
from api.v1.serializers import (
AttackSurfaceOverviewSerializer,
CategoryOverviewSerializer,
ComplianceOverviewAttributesSerializer,
ComplianceOverviewDetailSerializer,
ComplianceOverviewDetailThreatscoreSerializer,
@@ -357,7 +359,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.16.0"
spectacular_settings.VERSION = "1.17.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -2760,12 +2762,15 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
queryset = ResourceScanSummary.objects.filter(tenant_id=tenant_id)
scan_based_filters = {}
category_scan_filters = {} # Filters for ScanCategorySummary
if scans := query_params.get("filter[scan__in]") or query_params.get(
"filter[scan]"
):
queryset = queryset.filter(scan_id__in=scans.split(","))
scan_based_filters = {"id__in": scans.split(",")}
scan_ids_list = scans.split(",")
queryset = queryset.filter(scan_id__in=scan_ids_list)
scan_based_filters = {"id__in": scan_ids_list}
category_scan_filters = {"scan_id__in": scan_ids_list}
else:
exact = query_params.get("filter[inserted_at]")
gte = query_params.get("filter[inserted_at__gte]")
@@ -2809,6 +2814,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
scan_based_filters = {
key.lstrip("scan_"): value for key, value in date_filters.items()
}
category_scan_filters = date_filters
# ToRemove: Temporary fallback mechanism
if not queryset.exists():
@@ -2855,10 +2861,31 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
.order_by("resource_type")
)
# Get categories from ScanCategorySummary using same scan filters
categories = list(
ScanCategorySummary.objects.filter(
tenant_id=tenant_id, **category_scan_filters
)
.values_list("category", flat=True)
.distinct()
.order_by("category")
)
# Fallback to finding aggregation if no ScanCategorySummary exists
if not categories:
categories_set = set()
for categories_list in filtered_queryset.values_list(
"categories", flat=True
):
if categories_list:
categories_set.update(categories_list)
categories = sorted(categories_set)
result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
"categories": categories,
}
serializer = self.get_serializer(data=result)
@@ -2963,10 +2990,36 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
.order_by("resource_type")
)
# Get categories from ScanCategorySummary for latest scans
categories = list(
ScanCategorySummary.objects.filter(
tenant_id=tenant_id,
scan_id__in=latest_scans_queryset.values_list("id", flat=True),
)
.values_list("category", flat=True)
.distinct()
.order_by("category")
)
# Fallback to finding aggregation if no ScanCategorySummary exists
if not categories:
filtered_queryset = self.filter_queryset(self.get_queryset()).filter(
tenant_id=tenant_id,
scan_id__in=latest_scans_queryset.values_list("id", flat=True),
)
categories_set = set()
for categories_list in filtered_queryset.values_list(
"categories", flat=True
):
if categories_list:
categories_set.update(categories_list)
categories = sorted(categories_set)
result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
"categories": categories,
}
serializer = self.get_serializer(data=result)
@@ -4026,32 +4079,19 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
summary="Get attack surface overview",
description="Retrieve aggregated attack surface metrics from latest completed scans per provider.",
tags=["Overview"],
parameters=[
OpenApiParameter(
name="filter[provider_id]",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Filter by specific provider ID",
),
OpenApiParameter(
name="filter[provider_id.in]",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider IDs (comma-separated UUIDs)",
),
OpenApiParameter(
name="filter[provider_type]",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by provider type (aws, azure, gcp, etc.)",
),
OpenApiParameter(
name="filter[provider_type.in]",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider types (comma-separated)",
),
],
filters=True,
responses={200: AttackSurfaceOverviewSerializer(many=True)},
),
categories=extend_schema(
summary="Get category overview",
description=(
"Retrieve aggregated category metrics from latest completed scans per provider. "
"Returns one row per category with total, failed, and new failed findings counts, "
"plus a severity breakdown showing failed findings per severity level. "
),
tags=["Overview"],
filters=True,
responses={200: CategoryOverviewSerializer(many=True)},
),
)
@method_decorator(CACHE_DECORATOR, name="list")
@@ -4100,6 +4140,8 @@ class OverviewViewSet(BaseRLSViewSet):
return ThreatScoreSnapshotSerializer
elif self.action == "attack_surface":
return AttackSurfaceOverviewSerializer
elif self.action == "categories":
return CategoryOverviewSerializer
return super().get_serializer_class()
def get_filterset_class(self):
@@ -4111,6 +4153,10 @@ class OverviewViewSet(BaseRLSViewSet):
return ScanSummarySeverityFilter
elif self.action == "findings_severity_timeseries":
return DailySeveritySummaryFilter
elif self.action == "categories":
return CategoryOverviewFilter
elif self.action == "attack_surface":
return AttackSurfaceOverviewFilter
return None
def filter_queryset(self, queryset):
@@ -4196,29 +4242,41 @@ class OverviewViewSet(BaseRLSViewSet):
filterset = filterset_class(normalized_params, queryset=queryset)
return filterset.qs
def _latest_scan_ids_for_allowed_providers(self, tenant_id):
def _latest_scan_ids_for_allowed_providers(self, tenant_id, provider_filters=None):
provider_filter = self._get_provider_filter()
queryset = Scan.all_objects.filter(
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
)
if provider_filters:
queryset = queryset.filter(**provider_filters)
return (
Scan.all_objects.filter(
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
)
.order_by("provider_id", "-inserted_at")
queryset.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
def _attack_surface_check_ids_by_provider_types(self, provider_types):
check_ids_by_type = {
attack_surface_type: set()
for attack_surface_type in AttackSurfaceOverview.AttackSurfaceTypeChoices.values
}
for provider_type in provider_types:
attack_surface_mapping = _get_attack_surface_mapping_from_provider(
provider_type=provider_type
)
for attack_surface_type, check_ids in attack_surface_mapping.items():
check_ids_by_type[attack_surface_type].update(check_ids)
return check_ids_by_type
def _extract_provider_filters_from_params(self):
"""Extract provider filters from query params to apply on Scan queryset."""
params = self.request.query_params
filters = {}
provider_id = params.get("filter[provider_id]")
if provider_id:
filters["provider_id"] = provider_id
provider_id_in = params.get("filter[provider_id__in]")
if provider_id_in:
filters["provider_id__in"] = provider_id_in.split(",")
provider_type = params.get("filter[provider_type]")
if provider_type:
filters["provider__provider"] = provider_type
provider_type_in = params.get("filter[provider_type__in]")
if provider_type_in:
filters["provider__provider__in"] = provider_type_in.split(",")
return filters
@action(detail=False, methods=["get"], url_name="providers")
def providers(self, request):
@@ -4825,22 +4883,13 @@ class OverviewViewSet(BaseRLSViewSet):
tenant_id = request.tenant_id
latest_scan_ids = self._latest_scan_ids_for_allowed_providers(tenant_id)
# Build base queryset and apply user filters via FilterSet
base_queryset = AttackSurfaceOverview.objects.filter(
tenant_id=tenant_id, scan_id__in=latest_scan_ids
)
filtered_queryset = self._apply_filterset(
base_queryset, AttackSurfaceOverviewFilter
)
provider_types = list(
filtered_queryset.values_list(
"scan__provider__provider", flat=True
).distinct()
)
attack_surface_check_ids = self._attack_surface_check_ids_by_provider_types(
provider_types
)
# Aggregate attack surface data
aggregation = filtered_queryset.values("attack_surface_type").annotate(
total_findings=Coalesce(Sum("total_findings"), 0),
failed_findings=Coalesce(Sum("failed_findings"), 0),
@@ -4863,12 +4912,71 @@ class OverviewViewSet(BaseRLSViewSet):
}
response_data = [
{
"attack_surface_type": key,
**value,
"check_ids": attack_surface_check_ids.get(key, []),
{"attack_surface_type": key, **value} for key, value in results.items()
]
return Response(
self.get_serializer(response_data, many=True).data,
status=status.HTTP_200_OK,
)
@action(detail=False, methods=["get"], url_name="categories")
def categories(self, request):
tenant_id = request.tenant_id
provider_filters = self._extract_provider_filters_from_params()
latest_scan_ids = self._latest_scan_ids_for_allowed_providers(
tenant_id, provider_filters
)
base_queryset = ScanCategorySummary.objects.filter(
tenant_id=tenant_id, scan_id__in=latest_scan_ids
)
provider_filter_keys = {
"provider_id",
"provider_id__in",
"provider_type",
"provider_type__in",
}
filtered_queryset = self._apply_filterset(
base_queryset, CategoryOverviewFilter, exclude_keys=provider_filter_keys
)
aggregation = (
filtered_queryset.values("category", "severity")
.annotate(
total=Coalesce(Sum("total_findings"), 0),
failed=Coalesce(Sum("failed_findings"), 0),
new_failed=Coalesce(Sum("new_failed_findings"), 0),
)
.order_by("category", "severity")
)
category_data = defaultdict(
lambda: {
"total_findings": 0,
"failed_findings": 0,
"new_failed_findings": 0,
"severity": {
"informational": 0,
"low": 0,
"medium": 0,
"high": 0,
"critical": 0,
},
}
for key, value in results.items()
)
for row in aggregation:
cat = row["category"]
sev = row["severity"]
category_data[cat]["total_findings"] += row["total"]
category_data[cat]["failed_findings"] += row["failed"]
category_data[cat]["new_failed_findings"] += row["new_failed"]
if sev in category_data[cat]["severity"]:
category_data[cat]["severity"][sev] = row["failed"]
response_data = [
{"category": cat, **data} for cat, data in sorted(category_data.items())
]
return Response(
-2
View File
@@ -19,8 +19,6 @@ PORT = env("DJANGO_PORT", default=8000)
# Server settings
bind = f"{BIND_ADDRESS}:{PORT}"
# TODO: Remove after the category filter is implemented
limit_request_line = 0
workers = env.int("DJANGO_WORKERS", default=multiprocessing.cpu_count() * 2 + 1)
reload = DEBUG
+136 -1
View File
@@ -11,7 +11,10 @@ from django.urls import reverse
from django_celery_results.models import TaskResult
from rest_framework import status
from rest_framework.test import APIClient
from tasks.jobs.backfill import backfill_resource_scan_summaries
from tasks.jobs.backfill import (
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
)
from api.db_utils import rls_transaction
from api.models import (
@@ -36,6 +39,7 @@ from api.models import (
SAMLConfiguration,
SAMLDomainIndex,
Scan,
ScanCategorySummary,
ScanSummary,
StateChoices,
StatusChoices,
@@ -1271,6 +1275,113 @@ def latest_scan_finding(authenticated_client, providers_fixture, resources_fixtu
return finding
@pytest.fixture(scope="function")
def findings_with_categories(scans_fixture, resources_fixture):
scan = scans_fixture[0]
resource = resources_fixture[0]
finding = Finding.objects.create(
tenant_id=scan.tenant_id,
uid="finding_with_categories_1",
scan=scan,
delta=None,
status=Status.FAIL,
status_extended="test status",
impact=Severity.critical,
impact_extended="test impact",
severity=Severity.critical,
raw_result={"status": Status.FAIL},
check_id="genai_check",
check_metadata={"CheckId": "genai_check"},
categories=["gen-ai", "security"],
first_seen_at="2024-01-02T00:00:00Z",
)
finding.add_resources([resource])
backfill_resource_scan_summaries(str(scan.tenant_id), str(scan.id))
return finding
@pytest.fixture(scope="function")
def findings_with_multiple_categories(scans_fixture, resources_fixture):
scan = scans_fixture[0]
resource1, resource2 = resources_fixture[:2]
finding1 = Finding.objects.create(
tenant_id=scan.tenant_id,
uid="finding_multi_cat_1",
scan=scan,
delta=None,
status=Status.FAIL,
status_extended="test status",
impact=Severity.critical,
impact_extended="test impact",
severity=Severity.critical,
raw_result={"status": Status.FAIL},
check_id="genai_check",
check_metadata={"CheckId": "genai_check"},
categories=["gen-ai", "security"],
first_seen_at="2024-01-02T00:00:00Z",
)
finding1.add_resources([resource1])
finding2 = Finding.objects.create(
tenant_id=scan.tenant_id,
uid="finding_multi_cat_2",
scan=scan,
delta=None,
status=Status.FAIL,
status_extended="test status 2",
impact=Severity.high,
impact_extended="test impact 2",
severity=Severity.high,
raw_result={"status": Status.FAIL},
check_id="iam_check",
check_metadata={"CheckId": "iam_check"},
categories=["iam", "security"],
first_seen_at="2024-01-02T00:00:00Z",
)
finding2.add_resources([resource2])
backfill_resource_scan_summaries(str(scan.tenant_id), str(scan.id))
return finding1, finding2
@pytest.fixture(scope="function")
def latest_scan_finding_with_categories(
authenticated_client, providers_fixture, resources_fixture
):
provider = providers_fixture[0]
tenant_id = str(providers_fixture[0].tenant_id)
resource = resources_fixture[0]
scan = Scan.objects.create(
name="latest completed scan with categories",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant_id,
)
finding = Finding.objects.create(
tenant_id=tenant_id,
uid="latest_finding_with_categories",
scan=scan,
delta="new",
status=Status.FAIL,
status_extended="test status",
impact=Severity.critical,
impact_extended="test impact",
severity=Severity.critical,
raw_result={"status": Status.FAIL},
check_id="genai_iam_check",
check_metadata={"CheckId": "genai_iam_check"},
categories=["gen-ai", "iam"],
first_seen_at="2024-01-02T00:00:00Z",
)
finding.add_resources([resource])
backfill_resource_scan_summaries(tenant_id, str(scan.id))
backfill_scan_category_summaries(tenant_id, str(scan.id))
return finding
@pytest.fixture(scope="function")
def latest_scan_resource(authenticated_client, providers_fixture):
provider = providers_fixture[0]
@@ -1485,6 +1596,30 @@ def create_attack_surface_overview():
return _create
@pytest.fixture
def create_scan_category_summary():
def _create(
tenant,
scan,
category,
severity,
total_findings=10,
failed_findings=5,
new_failed_findings=2,
):
return ScanCategorySummary.objects.create(
tenant=tenant,
scan=scan,
category=category,
severity=severity,
total_findings=total_findings,
failed_findings=failed_findings,
new_failed_findings=new_failed_findings,
)
return _create
def get_authorization_header(access_token: str) -> dict:
return {"Authorization": f"Bearer {access_token}"}
+1
View File
@@ -61,4 +61,5 @@ def schedule_provider_scan(provider_instance: Provider):
"tenant_id": str(provider_instance.tenant_id),
"provider_id": provider_id,
},
countdown=5, # Avoid race conditions between the worker and the database
)
+67
View File
@@ -3,6 +3,7 @@ from datetime import timedelta
from django.db.models import Sum
from django.utils import timezone
from tasks.jobs.scan import aggregate_category_counts
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
@@ -10,10 +11,12 @@ from api.models import (
ComplianceOverviewSummary,
ComplianceRequirementOverview,
DailySeveritySummary,
Finding,
Resource,
ResourceFindingMapping,
ResourceScanSummary,
Scan,
ScanCategorySummary,
ScanSummary,
StateChoices,
)
@@ -274,3 +277,67 @@ def backfill_daily_severity_summaries(tenant_id: str, days: int = None):
"updated": updated_count,
"total_days": len(latest_scans_by_day),
}
def backfill_scan_category_summaries(tenant_id: str, scan_id: str):
"""
Backfill ScanCategorySummary for a completed scan.
Aggregates category counts from all findings in the scan and creates
one ScanCategorySummary row per (category, severity) combination.
Args:
tenant_id: Target tenant UUID
scan_id: Scan UUID to backfill
Returns:
dict: Status indicating whether backfill was performed
"""
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
if ScanCategorySummary.objects.filter(
tenant_id=tenant_id, scan_id=scan_id
).exists():
return {"status": "already backfilled"}
if not Scan.objects.filter(
tenant_id=tenant_id,
id=scan_id,
state__in=(StateChoices.COMPLETED, StateChoices.FAILED),
).exists():
return {"status": "scan is not completed"}
category_counts: dict[tuple[str, str], dict[str, int]] = {}
for finding in Finding.all_objects.filter(
tenant_id=tenant_id, scan_id=scan_id
).values("categories", "severity", "status", "delta", "muted"):
aggregate_category_counts(
categories=finding.get("categories") or [],
severity=finding.get("severity"),
status=finding.get("status"),
delta=finding.get("delta"),
muted=finding.get("muted", False),
cache=category_counts,
)
if not category_counts:
return {"status": "no categories to backfill"}
category_summaries = [
ScanCategorySummary(
tenant_id=tenant_id,
scan_id=scan_id,
category=category,
severity=severity,
total_findings=counts["total"],
failed_findings=counts["failed"],
new_failed_findings=counts["new_failed"],
)
for (category, severity), counts in category_counts.items()
]
with rls_transaction(tenant_id):
ScanCategorySummary.objects.bulk_create(
category_summaries, batch_size=500, ignore_conflicts=True
)
return {"status": "backfilled", "categories_count": len(category_counts)}
@@ -11,6 +11,41 @@ from api.models import LighthouseProviderConfiguration, LighthouseProviderModels
logger = get_task_logger(__name__)
# OpenAI model prefixes to exclude from Lighthouse model selection.
# These models don't support text chat completions and tool calling.
EXCLUDED_OPENAI_MODEL_PREFIXES = (
"dall-e", # Image generation
"whisper", # Audio transcription
"tts-", # Text-to-speech (tts-1, tts-1-hd, etc.)
"sora", # Text-to-video (sora-2, sora-2-pro, etc.)
"text-embedding", # Embeddings
"embedding", # Embeddings (alternative naming)
"text-moderation", # Content moderation
"omni-moderation", # Content moderation
"text-davinci", # Legacy completion models
"text-curie", # Legacy completion models
"text-babbage", # Legacy completion models
"text-ada", # Legacy completion models
"davinci", # Legacy completion models
"curie", # Legacy completion models
"babbage", # Legacy completion models
"ada", # Legacy completion models
"computer-use", # Computer control agent
"gpt-image", # Image generation
"gpt-audio", # Audio models
"gpt-realtime", # Realtime voice API
)
# OpenAI model substrings to exclude (patterns that can appear anywhere in model ID).
# These patterns identify non-chat model variants.
EXCLUDED_OPENAI_MODEL_SUBSTRINGS = (
"-audio-", # Audio preview models (gpt-4o-audio-preview, etc.)
"-realtime-", # Realtime preview models (gpt-4o-realtime-preview, etc.)
"-transcribe", # Transcription models (gpt-4o-transcribe, etc.)
"-tts", # TTS models (gpt-4o-mini-tts)
"-instruct", # Legacy instruct models (gpt-3.5-turbo-instruct, etc.)
)
def _extract_error_message(e: Exception) -> str:
"""
@@ -283,20 +318,41 @@ def _fetch_openai_models(api_key: str) -> Dict[str, str]:
"""
Fetch available models from OpenAI API.
Filters out models that don't support text input/output and tool calling,
such as image generation (DALL-E), audio transcription (Whisper),
text-to-speech (TTS), embeddings, and moderation models.
Args:
api_key: OpenAI API key for authentication.
Returns:
Dict mapping model_id to model_name. For OpenAI, both are the same
as the API doesn't provide separate display names.
as the API doesn't provide separate display names. Only includes
models that support text input, text output or tool calling.
Raises:
Exception: If the API call fails.
"""
client = openai.OpenAI(api_key=api_key)
models = client.models.list()
# OpenAI uses model.id for both ID and display name
return {m.id: m.id for m in getattr(models, "data", [])}
# Filter models to only include those supporting chat completions + tool calling
filtered_models = {}
for model in getattr(models, "data", []):
model_id = model.id
# Skip if model ID starts with excluded prefixes
if model_id.startswith(EXCLUDED_OPENAI_MODEL_PREFIXES):
continue
# Skip if model ID contains excluded substrings
if any(substring in model_id for substring in EXCLUDED_OPENAI_MODEL_SUBSTRINGS):
continue
# Include model (supports chat completions + tool calling)
filtered_models[model_id] = model_id
return filtered_models
def _fetch_openai_compatible_models(base_url: str, api_key: str) -> Dict[str, str]:
+75 -4
View File
@@ -8,6 +8,7 @@ from collections import defaultdict
from datetime import datetime, timezone
from typing import Any
import sentry_sdk
from celery.utils.log import get_task_logger
from config.env import env
from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS
@@ -39,6 +40,7 @@ from api.models import (
ResourceScanSummary,
ResourceTag,
Scan,
ScanCategorySummary,
ScanSummary,
StateChoices,
)
@@ -77,7 +79,6 @@ FINDINGS_MICRO_BATCH_SIZE = env.int("DJANGO_FINDINGS_MICRO_BATCH_SIZE", default=
# Controls how many rows each ORM bulk_create/bulk_update call sends to Postgres
SCAN_DB_BATCH_SIZE = env.int("DJANGO_SCAN_DB_BATCH_SIZE", default=500)
ATTACK_SURFACE_PROVIDER_COMPATIBILITY = {
"internet-exposed": None, # Compatible with all providers
"secrets": None, # Compatible with all providers
@@ -88,6 +89,40 @@ ATTACK_SURFACE_PROVIDER_COMPATIBILITY = {
_ATTACK_SURFACE_MAPPING_CACHE: dict[str, dict] = {}
def aggregate_category_counts(
categories: list[str],
severity: str,
status: str,
delta: str | None,
muted: bool,
cache: dict[tuple[str, str], dict[str, int]],
) -> None:
"""
Increment category counters in-place for a finding.
Args:
categories: List of categories from finding metadata.
severity: Severity level (e.g., "high", "medium").
status: Finding status as string ("FAIL", "PASS").
delta: Delta value as string ("new", "changed") or None.
muted: Whether the finding is muted.
cache: Dict {(category, severity): {"total", "failed", "new_failed"}} to update.
"""
is_failed = status == "FAIL" and not muted
is_new_failed = is_failed and delta == "new"
for cat in categories:
key = (cat, severity)
if key not in cache:
cache[key] = {"total": 0, "failed": 0, "new_failed": 0}
if not muted:
cache[key]["total"] += 1
if is_failed:
cache[key]["failed"] += 1
if is_new_failed:
cache[key]["new_failed"] += 1
def _get_attack_surface_mapping_from_provider(provider_type: str) -> dict:
global _ATTACK_SURFACE_MAPPING_CACHE
@@ -398,6 +433,7 @@ def _process_finding_micro_batch(
unique_resources: set,
scan_resource_cache: set,
mute_rules_cache: dict,
scan_categories_cache: dict[tuple[str, str], dict[str, int]],
) -> None:
"""
Process a micro-batch of findings and persist them using bulk operations.
@@ -418,6 +454,7 @@ def _process_finding_micro_batch(
unique_resources: Set tracking (uid, region) pairs seen in the scan.
scan_resource_cache: Set of tuples used to create `ResourceScanSummary` rows.
mute_rules_cache: Map of finding UID -> mute reason gathered before the scan.
scan_categories_cache: Dict tracking category counts {(category, severity): {"total", "failed", "new_failed"}}.
"""
# Accumulate objects for bulk operations
findings_to_create = []
@@ -573,11 +610,12 @@ def _process_finding_micro_batch(
resource_failed_findings_cache[resource_uid] += 1
# Create finding object (don't save yet)
check_metadata = finding.get_metadata()
finding_instance = Finding(
tenant_id=tenant_id,
uid=finding_uid,
delta=delta,
check_metadata=finding.get_metadata(),
check_metadata=check_metadata,
status=status,
status_extended=finding.status_extended,
severity=finding.severity,
@@ -590,6 +628,7 @@ def _process_finding_micro_batch(
muted_at=datetime.now(tz=timezone.utc) if is_muted else None,
muted_reason=muted_reason,
compliance=finding.compliance,
categories=check_metadata.get("categories", []) or [],
)
findings_to_create.append(finding_instance)
resource_denormalized_data.append((finding_instance, resource_instance))
@@ -604,6 +643,16 @@ def _process_finding_micro_batch(
)
)
# Track categories with counts for ScanCategorySummary by (category, severity)
aggregate_category_counts(
categories=check_metadata.get("categories", []) or [],
severity=finding.severity.value,
status=status.value,
delta=delta.value if delta else None,
muted=is_muted,
cache=scan_categories_cache,
)
# Bulk operations within single transaction
with rls_transaction(tenant_id):
# Bulk create findings
@@ -703,6 +752,7 @@ def perform_prowler_scan(
exception = None
unique_resources = set()
scan_resource_cache: set[tuple[str, str, str, str]] = set()
scan_categories_cache: dict[tuple[str, str], dict[str, int]] = {}
start_time = time.time()
exc = None
@@ -792,6 +842,7 @@ def perform_prowler_scan(
unique_resources=unique_resources,
scan_resource_cache=scan_resource_cache,
mute_rules_cache=mute_rules_cache,
scan_categories_cache=scan_categories_cache,
)
# Update scan progress
@@ -851,13 +902,33 @@ def perform_prowler_scan(
resource_scan_summaries, batch_size=500, ignore_conflicts=True
)
except Exception as filter_exception:
import sentry_sdk
sentry_sdk.capture_exception(filter_exception)
logger.error(
f"Error storing filter values for scan {scan_id}: {filter_exception}"
)
try:
if scan_categories_cache:
category_summaries = [
ScanCategorySummary(
tenant_id=tenant_id,
scan_id=scan_id,
category=category,
severity=severity,
total_findings=counts["total"],
failed_findings=counts["failed"],
new_failed_findings=counts["new_failed"],
)
for (category, severity), counts in scan_categories_cache.items()
]
with rls_transaction(tenant_id):
ScanCategorySummary.objects.bulk_create(
category_summaries, batch_size=500, ignore_conflicts=True
)
except Exception as cat_exception:
sentry_sdk.capture_exception(cat_exception)
logger.error(f"Error storing categories for scan {scan_id}: {cat_exception}")
serializer = ScanTaskSerializer(instance=scan_instance)
return serializer.data
+16
View File
@@ -12,6 +12,7 @@ from tasks.jobs.backfill import (
backfill_compliance_summaries,
backfill_daily_severity_summaries,
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
)
from tasks.jobs.connection import (
check_integration_connection,
@@ -534,6 +535,21 @@ def backfill_daily_severity_summaries_task(tenant_id: str, days: int = None):
return backfill_daily_severity_summaries(tenant_id=tenant_id, days=days)
@shared_task(name="backfill-scan-category-summaries", queue="backfill")
@handle_provider_deletion
def backfill_scan_category_summaries_task(tenant_id: str, scan_id: str):
"""
Backfill ScanCategorySummary for a completed scan.
Aggregates unique categories from findings and creates a summary row.
Args:
tenant_id (str): The tenant identifier.
scan_id (str): The scan identifier.
"""
return backfill_scan_category_summaries(tenant_id=tenant_id, scan_id=scan_id)
@shared_task(base=RLSTask, name="scan-compliance-overviews", queue="compliance")
@handle_provider_deletion
def create_compliance_requirements_task(tenant_id: str, scan_id: str):
@@ -4,14 +4,19 @@ import pytest
from tasks.jobs.backfill import (
backfill_compliance_summaries,
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
)
from api.models import (
ComplianceOverviewSummary,
Finding,
ResourceScanSummary,
Scan,
ScanCategorySummary,
StateChoices,
)
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
@pytest.fixture(scope="function")
@@ -46,6 +51,45 @@ def get_not_completed_scans(providers_fixture):
return scan_1, scan_2
@pytest.fixture(scope="function")
def findings_with_categories_fixture(scans_fixture, resources_fixture):
scan = scans_fixture[0]
resource = resources_fixture[0]
finding = Finding.objects.create(
tenant_id=scan.tenant_id,
uid="finding_with_categories",
scan=scan,
delta="new",
status=Status.FAIL,
status_extended="test status",
impact=Severity.critical,
impact_extended="test impact",
severity=Severity.critical,
raw_result={"status": Status.FAIL},
check_id="test_check",
check_metadata={"CheckId": "test_check"},
categories=["gen-ai", "security"],
first_seen_at="2024-01-02T00:00:00Z",
)
finding.add_resources([resource])
return finding
@pytest.fixture(scope="function")
def scan_category_summary_fixture(scans_fixture):
scan = scans_fixture[0]
return ScanCategorySummary.objects.create(
tenant_id=scan.tenant_id,
scan=scan,
category="existing-category",
severity=Severity.critical,
total_findings=1,
failed_findings=0,
new_failed_findings=0,
)
@pytest.mark.django_db
class TestBackfillResourceScanSummaries:
def test_already_backfilled(self, resource_scan_summary_data):
@@ -172,3 +216,47 @@ class TestBackfillComplianceSummaries:
assert summary.requirements_failed == expected_counts["requirements_failed"]
assert summary.requirements_manual == expected_counts["requirements_manual"]
assert summary.total_requirements == expected_counts["total_requirements"]
@pytest.mark.django_db
class TestBackfillScanCategorySummaries:
def test_already_backfilled(self, scan_category_summary_fixture):
tenant_id = scan_category_summary_fixture.tenant_id
scan_id = scan_category_summary_fixture.scan_id
result = backfill_scan_category_summaries(str(tenant_id), str(scan_id))
assert result == {"status": "already backfilled"}
def test_not_completed_scan(self, get_not_completed_scans):
for scan in get_not_completed_scans:
result = backfill_scan_category_summaries(str(scan.tenant_id), str(scan.id))
assert result == {"status": "scan is not completed"}
def test_no_categories_to_backfill(self, scans_fixture):
scan = scans_fixture[1] # Failed scan with no findings
result = backfill_scan_category_summaries(str(scan.tenant_id), str(scan.id))
assert result == {"status": "no categories to backfill"}
def test_successful_backfill(self, findings_with_categories_fixture):
finding = findings_with_categories_fixture
tenant_id = str(finding.tenant_id)
scan_id = str(finding.scan_id)
result = backfill_scan_category_summaries(tenant_id, scan_id)
# 2 categories × 1 severity = 2 rows
assert result == {"status": "backfilled", "categories_count": 2}
summaries = ScanCategorySummary.objects.filter(
tenant_id=tenant_id, scan_id=scan_id
)
assert summaries.count() == 2
categories = set(summaries.values_list("category", flat=True))
assert categories == {"gen-ai", "security"}
for summary in summaries:
assert summary.severity == Severity.critical
assert summary.total_findings == 1
assert summary.failed_findings == 1
assert summary.new_failed_findings == 1
+1
View File
@@ -28,6 +28,7 @@ class TestScheduleProviderScan:
"tenant_id": str(provider_instance.tenant_id),
"provider_id": str(provider_instance.id),
},
countdown=5,
)
task_name = f"scan-perform-scheduled-{provider_instance.id}"
+266
View File
@@ -20,6 +20,7 @@ from tasks.jobs.scan import (
_process_finding_micro_batch,
_store_resources,
aggregate_attack_surface,
aggregate_category_counts,
aggregate_findings,
create_compliance_requirements,
perform_prowler_scan,
@@ -1377,6 +1378,7 @@ class TestProcessFindingMicroBatch:
unique_resources: set[tuple[str, str]] = set()
scan_resource_cache: set[tuple[str, str, str, str]] = set()
mute_rules_cache = {}
scan_categories_cache: dict[tuple[str, str], dict[str, int]] = {}
with (
patch("tasks.jobs.scan.rls_transaction", new=noop_rls_transaction),
@@ -1394,6 +1396,7 @@ class TestProcessFindingMicroBatch:
unique_resources,
scan_resource_cache,
mute_rules_cache,
scan_categories_cache,
)
created_finding = Finding.objects.get(uid=finding.uid)
@@ -1486,6 +1489,7 @@ class TestProcessFindingMicroBatch:
unique_resources: set[tuple[str, str]] = set()
scan_resource_cache: set[tuple[str, str, str, str]] = set()
mute_rules_cache = {finding.uid: "Muted via rule"}
scan_categories_cache: dict[tuple[str, str], dict[str, int]] = {}
with (
patch("tasks.jobs.scan.rls_transaction", new=noop_rls_transaction),
@@ -1503,6 +1507,7 @@ class TestProcessFindingMicroBatch:
unique_resources,
scan_resource_cache,
mute_rules_cache,
scan_categories_cache,
)
existing_resource.refresh_from_db()
@@ -1610,6 +1615,7 @@ class TestProcessFindingMicroBatch:
unique_resources: set[tuple[str, str]] = set()
scan_resource_cache: set[tuple[str, str, str, str]] = set()
mute_rules_cache = {}
scan_categories_cache: dict[tuple[str, str], dict[str, int]] = {}
with (
patch("tasks.jobs.scan.rls_transaction", new=noop_rls_transaction),
@@ -1628,6 +1634,7 @@ class TestProcessFindingMicroBatch:
unique_resources,
scan_resource_cache,
mute_rules_cache,
scan_categories_cache,
)
# Verify the long UID finding was NOT created
@@ -1648,6 +1655,118 @@ class TestProcessFindingMicroBatch:
for call in warning_calls
)
def test_process_finding_micro_batch_tracks_categories(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
finding1 = FakeFinding(
uid="finding-cat-1",
status=StatusChoices.PASS,
status_extended="all good",
severity=Severity.low,
check_id="genai_check",
resource_uid="arn:aws:bedrock:::model/test",
resource_name="test-model",
region="us-east-1",
service_name="bedrock",
resource_type="model",
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={"categories": ["gen-ai", "security"]},
muted=False,
)
finding2 = FakeFinding(
uid="finding-cat-2",
status=StatusChoices.FAIL,
status_extended="bad",
severity=Severity.high,
check_id="iam_check",
resource_uid="arn:aws:iam:::user/test",
resource_name="test-user",
region="us-east-1",
service_name="iam",
resource_type="user",
resource_tags={},
resource_metadata={},
resource_details={},
partition="aws",
raw={},
compliance={},
metadata={"categories": ["security", "iam"]},
muted=False,
)
resource_cache = {}
tag_cache = {}
last_status_cache = {}
resource_failed_findings_cache = {}
unique_resources: set[tuple[str, str]] = set()
scan_resource_cache: set[tuple[str, str, str, str]] = set()
mute_rules_cache = {}
scan_categories_cache: dict[tuple[str, str], dict[str, int]] = {}
with (
patch("tasks.jobs.scan.rls_transaction", new=noop_rls_transaction),
patch("api.db_utils.rls_transaction", new=noop_rls_transaction),
):
_process_finding_micro_batch(
str(tenant.id),
[finding1, finding2],
scan,
provider,
resource_cache,
tag_cache,
last_status_cache,
resource_failed_findings_cache,
unique_resources,
scan_resource_cache,
mute_rules_cache,
scan_categories_cache,
)
# finding1: PASS, severity=low, categories=["gen-ai", "security"]
# finding2: FAIL, severity=high, categories=["security", "iam"]
# Keys are (category, severity) tuples
assert set(scan_categories_cache.keys()) == {
("gen-ai", "low"),
("security", "low"),
("security", "high"),
("iam", "high"),
}
assert scan_categories_cache[("gen-ai", "low")] == {
"total": 1,
"failed": 0,
"new_failed": 0,
}
assert scan_categories_cache[("security", "low")] == {
"total": 1,
"failed": 0,
"new_failed": 0,
}
assert scan_categories_cache[("security", "high")] == {
"total": 1,
"failed": 1,
"new_failed": 1,
}
assert scan_categories_cache[("iam", "high")] == {
"total": 1,
"failed": 1,
"new_failed": 1,
}
created_finding1 = Finding.objects.get(uid="finding-cat-1")
created_finding2 = Finding.objects.get(uid="finding-cat-2")
assert set(created_finding1.categories) == {"gen-ai", "security"}
assert set(created_finding2.categories) == {"security", "iam"}
@pytest.mark.django_db
class TestCreateComplianceRequirements:
@@ -3756,3 +3875,150 @@ class TestAggregateAttackSurface:
aggregate_attack_surface(str(tenant.id), str(scan.id))
mock_select_related.assert_called_once_with("provider")
class TestAggregateCategoryCounts:
"""Test aggregate_category_counts helper function."""
def test_aggregate_category_counts_basic(self):
"""Test basic category counting for a non-muted PASS finding."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=["security", "iam"],
severity="high",
status="PASS",
delta=None,
muted=False,
cache=cache,
)
assert ("security", "high") in cache
assert ("iam", "high") in cache
assert cache[("security", "high")] == {"total": 1, "failed": 0, "new_failed": 0}
assert cache[("iam", "high")] == {"total": 1, "failed": 0, "new_failed": 0}
def test_aggregate_category_counts_fail_not_muted(self):
"""Test category counting for a non-muted FAIL finding."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=["security"],
severity="critical",
status="FAIL",
delta=None,
muted=False,
cache=cache,
)
assert cache[("security", "critical")] == {
"total": 1,
"failed": 1,
"new_failed": 0,
}
def test_aggregate_category_counts_new_fail(self):
"""Test category counting for a new FAIL finding (delta='new')."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=["gen-ai"],
severity="high",
status="FAIL",
delta="new",
muted=False,
cache=cache,
)
assert cache[("gen-ai", "high")] == {"total": 1, "failed": 1, "new_failed": 1}
def test_aggregate_category_counts_muted_finding(self):
"""Test that muted findings are excluded from all counts."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=["security"],
severity="high",
status="FAIL",
delta="new",
muted=True,
cache=cache,
)
assert cache[("security", "high")] == {"total": 0, "failed": 0, "new_failed": 0}
def test_aggregate_category_counts_accumulates(self):
"""Test that multiple calls accumulate counts."""
cache: dict[tuple[str, str], dict[str, int]] = {}
# First finding: PASS
aggregate_category_counts(
categories=["security"],
severity="high",
status="PASS",
delta=None,
muted=False,
cache=cache,
)
# Second finding: FAIL (new)
aggregate_category_counts(
categories=["security"],
severity="high",
status="FAIL",
delta="new",
muted=False,
cache=cache,
)
# Third finding: FAIL (changed)
aggregate_category_counts(
categories=["security"],
severity="high",
status="FAIL",
delta="changed",
muted=False,
cache=cache,
)
assert cache[("security", "high")] == {"total": 3, "failed": 2, "new_failed": 1}
def test_aggregate_category_counts_empty_categories(self):
"""Test with empty categories list."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=[],
severity="high",
status="FAIL",
delta="new",
muted=False,
cache=cache,
)
assert cache == {}
def test_aggregate_category_counts_changed_delta(self):
"""Test that changed delta increments failed but not new_failed."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=["iam"],
severity="medium",
status="FAIL",
delta="changed",
muted=False,
cache=cache,
)
assert cache[("iam", "medium")] == {"total": 1, "failed": 1, "new_failed": 0}
def test_aggregate_category_counts_multiple_categories_single_finding(self):
"""Test single finding with multiple categories."""
cache: dict[tuple[str, str], dict[str, int]] = {}
aggregate_category_counts(
categories=["security", "compliance", "data-protection"],
severity="low",
status="FAIL",
delta="new",
muted=False,
cache=cache,
)
assert len(cache) == 3
for cat in ["security", "compliance", "data-protection"]:
assert cache[(cat, "low")] == {"total": 1, "failed": 1, "new_failed": 1}
@@ -213,3 +213,5 @@ Also is important to keep all code examples as short as possible, including the
| software-supply-chain | Detects or prevents tampering, unauthorized packages, or third-party risks in software supply chain |
| e3 | M365-specific controls enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) |
| e5 | M365-specific controls enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) |
| privilege-escalation | Detects IAM policies or permissions that allow identities to elevate their privileges beyond their intended scope, potentially gaining administrator or higher-level access through specific action combinations |
| ec2-imdsv1 | Identifies EC2 instances using Instance Metadata Service version 1 (IMDSv1), which is vulnerable to SSRF attacks and should be replaced with IMDSv2 for enhanced security |
+447
View File
@@ -0,0 +1,447 @@
---
title: 'Extending the MCP Server'
---
This guide explains how to extend the Prowler MCP Server with new tools and features.
<Info>
**New to Prowler MCP Server?** Start with the user documentation:
- [Overview](/getting-started/products/prowler-mcp) - Key capabilities, use cases, and deployment options
- [Installation](/getting-started/installation/prowler-mcp) - Install locally or use the managed server
- [Configuration](/getting-started/basic-usage/prowler-mcp) - Configure Claude Desktop, Cursor, and other MCP hosts
- [Tools Reference](/getting-started/basic-usage/prowler-mcp-tools) - Complete list of all available tools
</Info>
## Introduction
The Prowler MCP Server brings the entire Prowler ecosystem to AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). It enables seamless integration with AI tools like Claude Desktop, Cursor, and other MCP clients.
The server follows a modular architecture with three independent sub-servers:
| Sub-Server | Auth Required | Description |
|------------|---------------|-------------|
| Prowler App | Yes | Full access to Prowler Cloud and Self-Managed features |
| Prowler Hub | No | Security checks catalog with **over 1000 checks**, fixers, and **70+ compliance frameworks** |
| Prowler Documentation | No | Full-text search and retrieval of official documentation |
<Note>
For a complete list of tools and their descriptions, see the [Tools Reference](/getting-started/basic-usage/prowler-mcp-tools).
</Note>
## Architecture Overview
The MCP Server architecture is illustrated in the [Overview documentation](/getting-started/products/prowler-mcp#mcp-server-architecture). AI assistants connect through the MCP protocol to access Prowler's three main components.
### Server Structure
The main server orchestrates three sub-servers with prefixed namespacing:
```
mcp_server/prowler_mcp_server/
├── server.py # Main orchestrator
├── main.py # CLI entry point
├── prowler_hub/
├── prowler_app/
│ ├── tools/ # Tool implementations
│ ├── models/ # Pydantic models
│ └── utils/ # API client, auth, loader
└── prowler_documentation/
```
### Tool Registration Patterns
The MCP Server uses two patterns for tool registration:
1. **Direct Decorators** (Prowler Hub/Docs): Tools are registered using `@mcp.tool()` decorators
2. **Auto-Discovery** (Prowler App): All public methods of `BaseTool` subclasses are auto-registered
## Adding Tools to Prowler App
### Step 1: Create the Tool Class
Create a new file or add to an existing file in `prowler_app/tools/`:
```python
# prowler_app/tools/new_feature.py
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.new_feature import (
FeatureListResponse,
DetailedFeature,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
class NewFeatureTools(BaseTool):
"""Tools for managing new features."""
async def list_features(
self,
status: str | None = Field(
default=None,
description="Filter by status (active, inactive, pending)"
),
page_size: int = Field(
default=50,
description="Number of results per page (1-100)"
),
) -> dict[str, Any]:
"""List all features with optional filtering.
Returns a lightweight list of features optimized for LLM consumption.
Use get_feature for complete information about a specific feature.
"""
# Validate parameters
self.api_client.validate_page_size(page_size)
# Build query parameters
params: dict[str, Any] = {"page[size]": page_size}
if status:
params["filter[status]"] = status
# Make API request
clean_params = self.api_client.build_filter_params(params)
response = await self.api_client.get("/api/v1/features", params=clean_params)
# Transform to LLM-friendly format
return FeatureListResponse.from_api_response(response).model_dump()
async def get_feature(
self,
feature_id: str = Field(description="The UUID of the feature"),
) -> dict[str, Any]:
"""Get detailed information about a specific feature.
Returns complete feature details including configuration and metadata.
"""
try:
response = await self.api_client.get(f"/api/v1/features/{feature_id}")
return DetailedFeature.from_api_response(response["data"]).model_dump()
except Exception as e:
self.logger.error(f"Failed to get feature {feature_id}: {e}")
return {"error": str(e), "status": "failed"}
```
### Step 2: Create the Models
Create corresponding models in `prowler_app/models/`:
```python
# prowler_app/models/new_feature.py
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
class SimplifiedFeature(MinimalSerializerMixin):
"""Lightweight feature for list operations."""
id: str = Field(description="Unique feature identifier")
name: str = Field(description="Feature name")
status: str = Field(description="Current status")
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedFeature":
"""Transform API response to simplified format."""
attributes = data.get("attributes", {})
return cls(
id=data["id"],
name=attributes["name"],
status=attributes["status"],
)
class DetailedFeature(SimplifiedFeature):
"""Extended feature with complete details."""
description: str | None = Field(default=None, description="Feature description")
configuration: dict[str, Any] | None = Field(default=None, description="Configuration")
created_at: str = Field(description="Creation timestamp")
updated_at: str = Field(description="Last update timestamp")
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "DetailedFeature":
"""Transform API response to detailed format."""
attributes = data.get("attributes", {})
return cls(
id=data["id"],
name=attributes["name"],
status=attributes["status"],
description=attributes.get("description"),
configuration=attributes.get("configuration"),
created_at=attributes["created_at"],
updated_at=attributes["updated_at"],
)
class FeatureListResponse(MinimalSerializerMixin):
"""Response wrapper for feature list operations."""
count: int = Field(description="Total number of features")
features: list[SimplifiedFeature] = Field(description="List of features")
@classmethod
def from_api_response(cls, response: dict[str, Any]) -> "FeatureListResponse":
"""Transform API response to list format."""
data = response.get("data", [])
features = [SimplifiedFeature.from_api_response(item) for item in data]
return cls(count=len(features), features=features)
```
### Step 3: Verify Auto-Discovery
No manual registration is needed. The `tool_loader.py` automatically discovers and registers all `BaseTool` subclasses. Verify your tool is loaded by checking the server logs:
```
INFO - Auto-registered 2 tools from NewFeatureTools
INFO - Loaded and registered: NewFeatureTools
```
## Adding Tools to Prowler Hub/Docs
For Prowler Hub or Documentation tools, use the `@mcp.tool()` decorator directly:
```python
# prowler_hub/server.py
from fastmcp import FastMCP
hub_mcp_server = FastMCP("prowler-hub")
@hub_mcp_server.tool()
async def get_new_artifact(
artifact_id: str,
) -> dict:
"""Fetch a specific artifact from Prowler Hub.
Args:
artifact_id: The unique identifier of the artifact
Returns:
Dictionary containing artifact details
"""
response = prowler_hub_client.get(f"/artifact/{artifact_id}")
response.raise_for_status()
return response.json()
```
## Model Design Patterns
### MinimalSerializerMixin
All models should use `MinimalSerializerMixin` to optimize responses for LLM consumption:
```python
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
class MyModel(MinimalSerializerMixin):
"""Model that excludes empty values from serialization."""
required_field: str
optional_field: str | None = None # Excluded if None
empty_list: list = [] # Excluded if empty
```
This mixin automatically excludes:
- `None` values
- Empty strings
- Empty lists
- Empty dictionaries
### Two-Tier Model Pattern
Use two-tier models for efficient responses:
- **Simplified**: Lightweight models for list operations
- **Detailed**: Extended models for single-item retrieval
```python
class SimplifiedItem(MinimalSerializerMixin):
"""Use for list operations - minimal fields."""
id: str
name: str
status: str
class DetailedItem(SimplifiedItem):
"""Use for get operations - extends simplified with details."""
description: str | None = None
configuration: dict | None = None
created_at: str
updated_at: str
```
### Factory Method Pattern
Always implement `from_api_response()` for API transformation:
```python
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "MyModel":
"""Transform API response to model.
This method handles the JSON:API format used by Prowler API,
extracting attributes and relationships as needed.
"""
attributes = data.get("attributes", {})
return cls(
id=data["id"],
name=attributes["name"],
# ... map other fields
)
```
## API Client Usage
The `ProwlerAPIClient` is a singleton that handles authentication and HTTP requests:
```python
class MyTools(BaseTool):
async def my_tool(self) -> dict:
# GET request
response = await self.api_client.get("/api/v1/endpoint", params={"key": "value"})
# POST request
response = await self.api_client.post(
"/api/v1/endpoint",
json_data={"data": {"type": "items", "attributes": {...}}}
)
# PATCH request
response = await self.api_client.patch(
f"/api/v1/endpoint/{id}",
json_data={"data": {"attributes": {...}}}
)
# DELETE request
response = await self.api_client.delete(f"/api/v1/endpoint/{id}")
```
### Helper Methods
The API client provides useful helper methods:
```python
# Validate page size (1-1000)
self.api_client.validate_page_size(page_size)
# Normalize date range with max days limit
date_range = self.api_client.normalize_date_range(date_from, date_to, max_days=2)
# Build filter parameters (handles type conversion)
clean_params = self.api_client.build_filter_params({
"filter[status]": "active",
"filter[severity__in]": ["high", "critical"], # Converts to comma-separated
"filter[muted]": True, # Converts to "true"
})
# Poll async task until completion
result = await self.api_client.poll_task_until_complete(
task_id=task_id,
timeout=60,
poll_interval=1.0
)
```
## Best Practices
### Tool Docstrings
Tool docstrings become description that is going to be read by the LLM. Provide clear usage instructions and common workflows:
```python
async def search_items(self, status: str = Field(...)) -> dict:
"""Search items with advanced filtering.
Returns a lightweight list optimized for LLM consumption.
Use get_item for complete details about a specific item.
Common workflows:
- Find critical items: status="critical"
- Find recent items: Use date_from parameter
"""
```
### Error Handling
Return structured error responses instead of raising exceptions:
```python
async def get_item(self, item_id: str) -> dict:
try:
response = await self.api_client.get(f"/api/v1/items/{item_id}")
return DetailedItem.from_api_response(response["data"]).model_dump()
except Exception as e:
self.logger.error(f"Failed to get item {item_id}: {e}")
return {"error": str(e), "status": "failed"}
```
### Parameter Descriptions
Use Pydantic `Field()` with clear descriptions. This also helps LLMs understand
the purpose of each parameter, so be as descriptive as possible:
```python
async def list_items(
self,
severity: list[str] = Field(
default=[],
description="Filter by severity levels (critical, high, medium, low)"
),
status: str | None = Field(
default=None,
description="Filter by status (PASS, FAIL, MANUAL)"
),
page_size: int = Field(
default=50,
description="Results per page"
),
) -> dict:
```
## Development Commands
```bash
# Navigate to MCP server directory
cd mcp_server
# Run in STDIO mode (default)
uv run prowler-mcp
# Run in HTTP mode
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8000
# Run with environment variables
PROWLER_APP_API_KEY="pk_xxx" uv run prowler-mcp
```
For complete installation and deployment options, see:
- [Installation Guide](/getting-started/installation/prowler-mcp#from-source-development) - Development setup instructions
- [Configuration Guide](/getting-started/basic-usage/prowler-mcp) - MCP client configuration
For development I recommend to use the [Model Context Protocol Inspector](https://github.com/modelcontextprotocol/inspector) as MCP client to test and debug your tools.
## Related Documentation
<CardGroup cols={2}>
<Card title="MCP Server Overview" icon="circle-info" href="/getting-started/products/prowler-mcp">
Key capabilities, use cases, and deployment options
</Card>
<Card title="Tools Reference" icon="wrench" href="/getting-started/basic-usage/prowler-mcp-tools">
Complete reference of all available tools
</Card>
<Card title="Prowler Hub" icon="database" href="/getting-started/products/prowler-hub">
Security checks and compliance frameworks catalog
</Card>
<Card title="Lighthouse AI" icon="robot" href="/getting-started/products/prowler-lighthouse-ai">
AI-powered security analyst
</Card>
</CardGroup>
## Additional Resources
- [MCP Protocol Specification](https://modelcontextprotocol.io) - Model Context Protocol details
- [Prowler API Documentation](https://api.prowler.com/api/v1/docs) - API reference
- [Prowler Hub API](https://hub.prowler.com/api/docs) - Hub API reference
- [GitHub Repository](https://github.com/prowler-cloud/prowler) - Source code
+10 -25
View File
@@ -19,9 +19,7 @@
"groups": [
{
"group": "Welcome",
"pages": [
"introduction"
]
"pages": ["introduction"]
},
{
"group": "Prowler Cloud",
@@ -51,9 +49,7 @@
},
{
"group": "Prowler Lighthouse AI",
"pages": [
"getting-started/products/prowler-lighthouse-ai"
]
"pages": ["getting-started/products/prowler-lighthouse-ai"]
},
{
"group": "Prowler MCP Server",
@@ -153,9 +149,7 @@
"user-guide/cli/tutorials/quick-inventory",
{
"group": "Tutorials",
"pages": [
"user-guide/cli/tutorials/parallel-execution"
]
"pages": ["user-guide/cli/tutorials/parallel-execution"]
}
]
},
@@ -243,9 +237,7 @@
},
{
"group": "LLM",
"pages": [
"user-guide/providers/llm/getting-started-llm"
]
"pages": ["user-guide/providers/llm/getting-started-llm"]
},
{
"group": "Oracle Cloud Infrastructure",
@@ -258,9 +250,7 @@
},
{
"group": "Compliance",
"pages": [
"user-guide/compliance/tutorials/threatscore"
]
"pages": ["user-guide/compliance/tutorials/threatscore"]
}
]
},
@@ -277,7 +267,8 @@
"developer-guide/outputs",
"developer-guide/integrations",
"developer-guide/security-compliance-framework",
"developer-guide/lighthouse"
"developer-guide/lighthouse",
"developer-guide/mcp-server"
]
},
{
@@ -313,21 +304,15 @@
},
{
"tab": "Security",
"pages": [
"security"
]
"pages": ["security"]
},
{
"tab": "Contact Us",
"pages": [
"contact"
]
"pages": ["contact"]
},
{
"tab": "Troubleshooting",
"pages": [
"troubleshooting"
]
"pages": ["troubleshooting"]
},
{
"tab": "About Us",
@@ -10,7 +10,7 @@ Complete reference guide for all tools available in the Prowler MCP Server. Tool
|----------|------------|------------------------|
| Prowler Hub | 10 tools | No |
| Prowler Documentation | 2 tools | No |
| Prowler Cloud/App | 28 tools | Yes |
| Prowler Cloud/App | 22 tools | Yes |
## Tool Naming Convention
@@ -20,6 +20,66 @@ All tools follow a consistent naming pattern with prefixes:
- `prowler_docs_*` - Prowler documentation search and retrieval
- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
## Prowler Cloud/App Tools
Manage Prowler Cloud or Prowler App (Self-Managed) features. **Requires authentication.**
<Note>
These tools require a valid API key. See the [Configuration Guide](/getting-started/basic-usage/prowler-mcp) for authentication setup.
</Note>
### Findings Management
Tools for searching, viewing, and analyzing security findings across all cloud providers.
- **`prowler_app_search_security_findings`** - Search and filter security findings with advanced filtering options (severity, status, provider, region, service, check ID, date range, muted status)
- **`prowler_app_get_finding_details`** - Get comprehensive details about a specific finding including remediation guidance, check metadata, and resource relationships
- **`prowler_app_get_findings_overview`** - Get aggregate statistics and trends about security findings as a markdown report
### Provider Management
Tools for managing cloud provider connections in Prowler.
- **`prowler_app_search_providers`** - Search and view configured providers with their connection status
- **`prowler_app_connect_provider`** - Register and connect a provider with credentials for security scanning
- **`prowler_app_delete_provider`** - Permanently remove a provider from Prowler
### Scan Management
Tools for managing and monitoring security scans.
- **`prowler_app_list_scans`** - List and filter security scans across all providers
- **`prowler_app_get_scan`** - Get comprehensive details about a specific scan (progress, duration, resource counts)
- **`prowler_app_trigger_scan`** - Trigger a manual security scan for a provider
- **`prowler_app_schedule_daily_scan`** - Schedule automated daily scans for continuous monitoring
- **`prowler_app_update_scan`** - Update scan name for better organization
### Resources Management
Tools for searching, viewing, and analyzing cloud resources discovered by Prowler.
- **`prowler_app_list_resources`** - List and filter cloud resources with advanced filtering options (provider, region, service, resource type, tags)
- **`prowler_app_get_resource`** - Get comprehensive details about a specific resource including configuration, metadata, and finding relationships
- **`prowler_app_get_resources_overview`** - Get aggregate statistics about cloud resources as a markdown report
### Muting Management
Tools for managing finding muting, including pattern-based bulk muting (mutelist) and finding-specific mute rules.
#### Mutelist (Pattern-Based Muting)
- **`prowler_app_get_mutelist`** - Retrieve the current mutelist configuration for the tenant
- **`prowler_app_set_mutelist`** - Create or update the mutelist configuration for pattern-based bulk muting
- **`prowler_app_delete_mutelist`** - Remove the mutelist configuration from the tenant
#### Mute Rules (Finding-Specific Muting)
- **`prowler_app_list_mute_rules`** - Search and filter mute rules with pagination support
- **`prowler_app_get_mute_rule`** - Retrieve comprehensive details about a specific mute rule
- **`prowler_app_create_mute_rule`** - Create a new mute rule to mute specific findings with documentation and audit trail
- **`prowler_app_update_mute_rule`** - Update a mute rule's name, reason, or enabled status
- **`prowler_app_delete_mute_rule`** - Delete a mute rule from the system
## Prowler Hub Tools
Access Prowler's security check catalog and compliance frameworks. **No authentication required.**
@@ -53,60 +113,6 @@ Search and access official Prowler documentation. **No authentication required.*
- **`prowler_docs_search`** - Search the official Prowler documentation using full-text search
- **`prowler_docs_get_document`** - Retrieve the full markdown content of a specific documentation file
## Prowler Cloud/App Tools
Manage Prowler Cloud or Prowler App (Self-Managed) features. **Requires authentication.**
<Note>
These tools require a valid API key. See the [Configuration Guide](/getting-started/basic-usage/prowler-mcp) for authentication setup.
</Note>
### Findings Management
- **`prowler_app_list_findings`** - List security findings with advanced filtering
- **`prowler_app_get_finding`** - Get detailed information about a specific finding
- **`prowler_app_get_latest_findings`** - Retrieve latest findings from the most recent scans
- **`prowler_app_get_findings_metadata`** - Get unique metadata values from filtered findings
- **`prowler_app_get_latest_findings_metadata`** - Get metadata from latest findings across all providers
### Provider Management
- **`prowler_app_list_providers`** - List all providers with filtering options
- **`prowler_app_create_provider`** - Create a new provider in the current tenant
- **`prowler_app_get_provider`** - Get detailed information about a specific provider
- **`prowler_app_update_provider`** - Update provider details (alias, etc.)
- **`prowler_app_delete_provider`** - Delete a specific provider
- **`prowler_app_test_provider_connection`** - Test provider connection status
### Provider Secrets Management
- **`prowler_app_list_provider_secrets`** - List all provider secrets with filtering
- **`prowler_app_add_provider_secret`** - Add or update credentials for a provider
- **`prowler_app_get_provider_secret`** - Get detailed information about a provider secret
- **`prowler_app_update_provider_secret`** - Update provider secret details
- **`prowler_app_delete_provider_secret`** - Delete a provider secret
### Scan Management
- **`prowler_app_list_scans`** - List all scans with filtering options
- **`prowler_app_create_scan`** - Trigger a manual scan for a specific provider
- **`prowler_app_get_scan`** - Get detailed information about a specific scan
- **`prowler_app_update_scan`** - Update scan details
- **`prowler_app_get_scan_compliance_report`** - Download compliance report as CSV
- **`prowler_app_get_scan_report`** - Download ZIP file containing complete scan report
### Schedule Management
- **`prowler_app_schedules_daily_scan`** - Create a daily scheduled scan for a provider
### Processor Management
- **`prowler_app_processors_list`** - List all processors with filtering
- **`prowler_app_processors_create`** - Create a new processor (currently only mute lists supported)
- **`prowler_app_processors_retrieve`** - Get processor details by ID
- **`prowler_app_processors_partial_update`** - Update processor configuration
- **`prowler_app_processors_destroy`** - Delete a processor
## Usage Tips
- Use natural language to interact with the tools through your AI assistant
@@ -139,7 +139,7 @@ STDIO mode is only available when running the MCP server locally.
"args": ["/absolute/path/to/prowler/mcp_server/"],
"env": {
"PROWLER_APP_API_KEY": "<your-api-key-here>",
"PROWLER_API_BASE_URL": "https://api.prowler.com"
"API_BASE_URL": "https://api.prowler.com/api/v1"
}
}
}
@@ -147,7 +147,7 @@ STDIO mode is only available when running the MCP server locally.
```
<Note>
Replace `/absolute/path/to/prowler/mcp_server/` with the actual path. The `PROWLER_API_BASE_URL` is optional and defaults to Prowler Cloud API.
Replace `/absolute/path/to/prowler/mcp_server/` with the actual path. The `API_BASE_URL` is optional and defaults to Prowler Cloud API.
</Note>
</Tab>
@@ -167,7 +167,7 @@ STDIO mode is only available when running the MCP server locally.
"--env",
"PROWLER_APP_API_KEY=<your-api-key-here>",
"--env",
"PROWLER_API_BASE_URL=https://api.prowler.com",
"API_BASE_URL=https://api.prowler.com/api/v1",
"prowlercloud/prowler-mcp"
]
}
@@ -176,7 +176,7 @@ STDIO mode is only available when running the MCP server locally.
```
<Note>
The `PROWLER_API_BASE_URL` is optional and defaults to Prowler Cloud API.
The `API_BASE_URL` is optional and defaults to Prowler Cloud API.
</Note>
</Tab>
@@ -52,7 +52,7 @@ Choose one of the following installation methods:
```bash
docker run --rm -i \
-e PROWLER_APP_API_KEY="pk_your_api_key" \
-e PROWLER_API_BASE_URL="https://api.prowler.com" \
-e API_BASE_URL="https://api.prowler.com/api/v1" \
prowlercloud/prowler-mcp
```
@@ -181,19 +181,19 @@ Configure the server using environment variables:
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `PROWLER_APP_API_KEY` | Prowler API key | Only for STDIO mode | - |
| `PROWLER_API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com` |
| `API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com/api/v1` |
| `PROWLER_MCP_TRANSPORT_MODE` | Default transport mode (overwritten by `--transport` argument) | No | `stdio` |
<CodeGroup>
```bash macOS/Linux
export PROWLER_APP_API_KEY="pk_your_api_key_here"
export PROWLER_API_BASE_URL="https://api.prowler.com"
export API_BASE_URL="https://api.prowler.com/api/v1"
export PROWLER_MCP_TRANSPORT_MODE="http"
```
```bash Windows PowerShell
$env:PROWLER_APP_API_KEY="pk_your_api_key_here"
$env:PROWLER_API_BASE_URL="https://api.prowler.com"
$env:API_BASE_URL="https://api.prowler.com/api/v1"
$env:PROWLER_MCP_TRANSPORT_MODE="http"
```
</CodeGroup>
@@ -208,7 +208,7 @@ For convenience, create a `.env` file in the `mcp_server` directory:
```bash .env
PROWLER_APP_API_KEY=pk_your_api_key_here
PROWLER_API_BASE_URL=https://api.prowler.com
API_BASE_URL=https://api.prowler.com/api/v1
PROWLER_MCP_TRANSPORT_MODE=stdio
```
@@ -6,7 +6,7 @@ title: "Overview"
**Why this matters**: Every engineer has asked, “What does this check actually do?” Prowler Hub answers that question in one place, lets you pin to a specific version, and pulls definitions into your own tools or dashboards.
![](/images/products/prowler-hub.webp)
![](/images/products/prowler-hub.png)
<Card title="Go to Prowler Hub" href="https://hub.prowler.com" />
@@ -14,4 +14,4 @@ Prowler Hub also provides a fully documented public API that you can integrate i
📚 Explore the API docs at: https://hub.prowler.com/api/docs
Whether youre customizing policies, managing compliance, or enhancing visibility, Prowler Hub is built to support your security operations.
Whether youre customizing policies, managing compliance, or enhancing visibility, Prowler Hub is built to support your security operations.
+13 -11
View File
@@ -19,12 +19,11 @@ The Prowler MCP Server provides three main integration points:
### 1. Prowler Cloud and Prowler App (Self-Managed)
Full access to Prowler Cloud platform and self-managed Prowler App for:
- **Provider Management**: Create, configure, and manage cloud providers (AWS, Azure, GCP, etc.).
- **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments.
- **Findings Analysis**: Query, filter, and analyze security findings across all your cloud environments.
- **Compliance Reporting**: Generate compliance reports for various frameworks (CIS, PCI-DSS, HIPAA, etc.).
- **Secrets Management**: Securely manage provider credentials and connection details.
- **Processor Configuration**: Set up the [Prowler Mutelist](/user-guide/tutorials/prowler-app-mute-findings) to mute findings.
- **Findings Analysis**: Query, filter, and analyze security findings across all your cloud environments
- **Provider Management**: Create, configure, and manage your configured Prowler providers (AWS, Azure, GCP, etc.)
- **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting lists/rules to suppress non-relevant findings
### 2. Prowler Hub
@@ -49,7 +48,10 @@ The following diagram illustrates the Prowler MCP Server architecture and its in
<img className="block dark:hidden" src="/images/prowler_mcp_schema_light.png" alt="Prowler MCP Server Schema" />
<img className="hidden dark:block" src="/images/prowler_mcp_schema_dark.png" alt="Prowler MCP Server Schema" />
The architecture shows how AI assistants connect through the MCP protocol to access Prowler's three main components: Prowler Cloud/App for security operations, Prowler Hub for security knowledge, and Prowler Documentation for guidance and reference.
The architecture shows how AI assistants connect through the MCP protocol to access Prowler's three main components:
- Prowler Cloud/App for security operations
- Prowler Hub for security knowledge
- Prowler Documentation for guidance and reference.
## Use Cases
@@ -57,12 +59,12 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
**Security Operations**
- "Show me all critical findings from my AWS production accounts"
- "What is my compliance status for the PCI standards accross all my AWS accounts according to the latest Prowler scan results?"
- "Register my new AWS account in Prowler and run an scheduled scan every day"
- "Register my new AWS account in Prowler and run a scheduled scan every day"
- "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity"
**Security Research**
- "Explain what the S3 bucket public access check does"
- "Find all checks related to encryption at rest"
- "Explain what the S3 bucket public access Prowler check does"
- "Find all Prowler checks related to encryption at rest"
- "What is the latest version of the CIS that Prowler is covering per provider?"
**Documentation & Learning**
Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

+74
View File
@@ -24,6 +24,80 @@ We enforce [pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pr
Our container registries are continuously scanned for vulnerabilities, with findings automatically reported to our security team for assessment and remediation. This process evolves alongside our stack as we adopt new languages, frameworks, and technologies, ensuring our security practices remain comprehensive, proactive, and adaptable.
### Static Application Security Testing (SAST)
We employ multiple SAST tools across our codebase to identify security vulnerabilities, code quality issues, and potential bugs during development:
#### CodeQL Analysis
- **Scope**: UI (JavaScript/TypeScript), API (Python), and SDK (Python)
- **Frequency**: On every push and pull request, plus daily scheduled scans
- **Integration**: Results uploaded to GitHub Security tab via SARIF format
- **Purpose**: Identifies security vulnerabilities, coding errors, and potential exploits in source code
#### Python Security Scanners
- **Bandit**: Detects common security issues in Python code (SQL injection, hardcoded passwords, etc.)
- Configured to ignore test files and report only high-severity issues
- Runs on both SDK and API codebases
- **Pylint**: Static code analysis with security-focused checks
- Integrated into pre-commit hooks and CI/CD pipelines
#### Code Quality & Dead Code Detection
- **Vulture**: Identifies unused code that could indicate incomplete implementations or security gaps
- **Flake8**: Style guide enforcement with security-relevant checks
- **Shellcheck**: Security and correctness checks for shell scripts
### Software Composition Analysis (SCA)
We continuously monitor our dependencies for known vulnerabilities and ensure timely updates:
#### Dependency Vulnerability Scanning
- **Safety**: Scans Python dependencies against known vulnerability databases
- Runs on every commit via pre-commit hooks
- Integrated into CI/CD for SDK and API
- Configured with selective ignores for tracked exceptions
- **Trivy**: Multi-purpose scanner for containers and dependencies
- Scans all container images (UI, API, SDK, MCP Server)
- Checks for vulnerabilities in OS packages and application dependencies
- Reports findings to GitHub Security tab
#### Automated Dependency Updates
- **Dependabot**: Automated pull requests for dependency updates
- **Python (pip)**: Monthly updates for SDK
- **GitHub Actions**: Monthly updates for workflow dependencies
- **Docker**: Monthly updates for base images
- Temporarily paused for API and UI to maintain stability during active development
- **Security-first approach**: Even when paused, Dependabot automatically creates pull requests for security vulnerabilities, ensuring critical security patches are never delayed
### Container Security
All container images are scanned before deployment:
- **Trivy Vulnerability Scanning**:
- Scans images for vulnerabilities and misconfigurations
- Generates SARIF reports uploaded to GitHub Security tab
- Creates PR comments with scan summaries
- Configurable to fail builds on critical findings
- Reports include CVE counts and remediation guidance
- **Hadolint**: Dockerfile linting to enforce best practices
- Validates Dockerfile syntax and structure
- Ensures secure image building practices
### Secrets Detection
We protect against accidental exposure of sensitive credentials:
- **TruffleHog**: Scans entire codebase and Git history for secrets
- Runs on every push and pull request
- Pre-commit hook prevents committing secrets
- Detects high-entropy strings, API keys, tokens, and credentials
- Configured to report verified and unknown findings
### Security Monitoring
- **GitHub Security Tab**: Centralized view of all security findings from CodeQL, Trivy, and other SARIF-compatible tools
- **Artifact Retention**: Security scan reports retained for post-deployment analysis
- **PR Comments**: Automated security feedback on pull requests for rapid remediation
## Reporting Vulnerabilities
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
@@ -5,18 +5,26 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler's Infrastructure as Code (IaC) provider enables scanning of local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks, allowing assessment of code before deployment.
## Supported Scanners
## Supported IaC Formats
The IaC provider leverages [Trivy](https://trivy.dev/latest/docs/scanner/vulnerability/) to support multiple scanners, including:
Prowler IaC provider scans the following Infrastructure as Code configurations for misconfigurations and secrets:
- Vulnerability
- Misconfiguration
- Secret
- License
| Configuration Type | File Patterns |
|--------------------|----------------------------------------------|
| Kubernetes | `*.yml`, `*.yaml`, `*.json` |
| Docker | `Dockerfile`, `Containerfile` |
| Terraform | `*.tf`, `*.tf.json`, `*.tfvars` |
| Terraform Plan | `tfplan`, `*.tfplan`, `*.json` |
| CloudFormation | `*.yml`, `*.yaml`, `*.json` |
| Azure ARM Template | `*.json` |
| Helm | `*.yml`, `*.yaml`, `*.tpl`, `*.tar.gz`, etc. |
| YAML | `*.yaml`, `*.yml` |
| JSON | `*.json` |
| Ansible | `*.yml`, `*.yaml`, `*.json`, `*.ini`, without extension |
## How It Works
- The IaC provider scans local directories (or specified paths) for supported IaC files, or scans remote repositories.
- Prowler App leverages [Trivy](https://trivy.dev/docs/latest/guide/coverage/iac/#scanner) to scan local directories (or specified paths) for supported IaC files, or scans remote repositories.
- No cloud credentials or authentication are required for local scans.
- For remote repository scans, authentication can be provided via [git URL](https://git-scm.com/docs/git-clone#_git_urls), CLI flags or environment variables.
- Check the [IaC Authentication](/user-guide/providers/iac/authentication) page for more details.
@@ -27,6 +35,10 @@ The IaC provider leverages [Trivy](https://trivy.dev/latest/docs/scanner/vulnera
<VersionBadge version="5.14.0" />
### Supported Scanners
Scanner selection is not configurable in Prowler App. Default scanners, misconfig and secret, run automatically during each scan.
### Step 1: Access Prowler Cloud/App
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
@@ -63,6 +75,17 @@ The IaC provider leverages [Trivy](https://trivy.dev/latest/docs/scanner/vulnera
<VersionBadge version="5.8.0" />
### Supported Scanners
Prowler CLI supports the following scanners:
- [Vulnerability](https://trivy.dev/docs/latest/guide/scanner/vulnerability/)
- [Misconfiguration](https://trivy.dev/docs/latest/guide/scanner/misconfiguration/)
- [Secret](https://trivy.dev/docs/latest/guide/scanner/secret/)
- [License](https://trivy.dev/docs/latest/guide/scanner/license/)
By default, only misconfiguration and secret scanners run during a scan. To specify which scanners to use, refer to the [Specify Scanners](#specify-scanners) section below.
### Usage
Use the `iac` argument to run Prowler with the IaC provider. Specify the directory or repository to scan, frameworks to include, and paths to exclude.
@@ -103,7 +126,7 @@ Authentication for private repositories can be provided using one of the followi
#### Specify Scanners
Scan only vulnerability and misconfiguration scanners:
To run only specific scanners, use the `--scanners` flag. For example, to scan only for vulnerabilities and misconfigurations:
```sh
prowler iac --scan-path ./my-iac-directory --scanners vuln misconfig
@@ -47,8 +47,43 @@ If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these
kubectl create token prowler-sa -n prowler-ns --duration=0
```
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=<TIME>` (e.g., `--duration=24h`).
- **Important:** If the token expires, Prowler Cloud will no longer be able to authenticate with the cluster. In this case, you will need to generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Choose an appropriate expiration time based on security policies. For a limited-time token, set `--duration=<TIME>` (e.g., `--duration=24h`).
<Note>
**Important:** If the token expires, Prowler Cloud can no longer authenticate with the cluster. Generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
</Note>
<Tip>
**Token Expiration Limits**
When the Kubernetes cluster has `--service-account-max-token-expiration` configured, any token requested with a duration exceeding the maximum allowed value (including `--duration=0`) is automatically reduced to the cluster's maximum token expiration time. As an alternative solution, create a legacy Secret manually. Although Kubernetes no longer creates these secrets automatically, manual creation and linking to a ServiceAccount is still supported. These tokens do not expire until the secret or ServiceAccount is deleted.
**Steps:**
1. Create a `secret-sa.yaml` file (or any preferred name) with the following content:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: prowler-token-long-lived
namespace: prowler-ns
annotations:
kubernetes.io/service-account.name: "prowler-sa"
type: kubernetes.io/service-account-token
```
2. Apply the secret:
```console
kubectl apply -f secret-sa.yaml
```
3. Retrieve the token (which will be permanent):
```console
kubectl get secret prowler-token-long-lived -n prowler-ns -o jsonpath='{.data.token}' | base64 --decode
```
</Tip>
3. Update your `kubeconfig` to use the ServiceAccount token:
@@ -2,18 +2,83 @@
title: 'Getting Started with MongoDB Atlas'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler supports MongoDB Atlas both from the CLI and from Prowler Cloud. This guide walks you through the requirements, how to connect the provider in the UI, and how to run scans from the command line.
## Prerequisites
Before you begin, make sure you have:
1. A MongoDB Atlas organization with **API Access** enabled.
2. An **Organization ID** (24-character hex string).
3. An **API Key pair** (public and private keys) with appropriate permissions:
- **Organization Read Only**: Provides read-only access to everything in the organization, including all projects in the organization. This permission is sufficient for most security checks.
- **Organization Owner**: Required to audit the [Auditing configuration](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-auditing) for projects. Database auditing tracks database operations and security events, including authentication attempts, data definition language (DDL) changes, user and role modifications, and privilege grants. This configuration is essential for security monitoring, forensics, and compliance. Without **Organization Owner** permission, the `projects_auditing_enabled` check cannot retrieve the audit configuration status.
4. Prowler App access (cloud or self-hosted) or the Prowler CLI (`pip install prowler`).
For detailed instructions on creating API keys, see the [MongoDB Atlas authentication guide](./authentication.mdx).
<Warning>
If **Require IP Access List for the Atlas Administration API** is enabled in your organization settings, you **must** add the IP address of the host running Prowler (or the public IP of Prowler Cloud) to the organization IP Access List or Atlas will reject every API call. You can manage this under **Settings → Organization Settings → Security**. See step 7 of the [authentication guide](./authentication.mdx) for detailed instructions, and refer to the [Prowler Cloud public IP list](../../tutorials/prowler-cloud-public-ips) when using Prowler Cloud.
</Warning>
<CardGroup cols={2}>
<Card title="Prowler Cloud" icon="cloud" href="#prowler-cloud">
Onboard MongoDB Atlas using Prowler Cloud
</Card>
<Card title="Prowler CLI" icon="terminal" href="#prowler-cli">
Onboard MongoDB Atlas using Prowler CLI
</Card>
</CardGroup>
## Prowler Cloud
<VersionBadge version="5.15.0" />
### Step 1: Add the provider
1. Navigate to **Cloud Providers** and click **Add Cloud Provider**.
![Add provider list](./img/add-provider-list.png)
2. Select **MongoDB Atlas** from the provider list.
3. Enter your **Organization ID** (24 hex characters). This value is visible in the Atlas UI under **Organization Settings**.
![Add organization ID](./img/add-org-id.png)
4. (Optional) Add a friendly alias to identify this organization in dashboards.
### Step 2: Provide API credentials
1. Click **Next** to open the credentials form.
2. Paste the **Atlas Public Key** and **Atlas Private Key** generated in the Atlas console.
![Add credentials](./img/add-credentials.png)
### Step 3: Test the connection and start scanning
1. Click **Test connection** to ensure Prowler App can reach the Atlas API.
2. Save the credentials. The provider will appear in the list with its current connection status.
3. Launch a scan from the provider row or from the **Scans** page.
![Launch scan](./img/launch-scan.png)
---
## Prowler CLI
### Authentication Methods
<VersionBadge version="5.12.0" />
#### Command-Line Arguments
You can also run MongoDB Atlas assessments directly from the CLI. Both command-line flags and environment variables are supported.
### Step 1: Select an authentication method
Choose one of the following authentication methods:
#### Command-line arguments
```bash
prowler mongodbatlas --atlas-public-key <public_key> --atlas-private-key <private_key>
prowler mongodbatlas \
--atlas-public-key <public_key> \
--atlas-private-key <private_key>
```
#### Environment Variables
#### Environment variables
```bash
export ATLAS_PUBLIC_KEY=<public_key>
@@ -21,33 +86,28 @@ export ATLAS_PRIVATE_KEY=<private_key>
prowler mongodbatlas
```
### Step 2: Run the first scan
### Scan All Projects and Clusters
After storing API keys, run Prowler with the following command:
```bash
prowler mongodbatlas --atlas-public-key <key> --atlas-private-key <secret>
```
Alternatively, set API keys as environment variables:
```bash
export ATLAS_PUBLIC_KEY=<key>
export ATLAS_PRIVATE_KEY=<secret>
```
Then run Prowler with the following command:
#### Scan all projects and clusters
```bash
prowler mongodbatlas
```
### Scanning a Specific Project
This command enumerates all projects accessible to the API key and scans every cluster.
To scan a specific project, add the following argument to the command above:
#### Scan a specific project
Add the `--atlas-project-id` flag when you only want to assess one project:
```bash
prowler mongodbatlas --atlas-project-id <project-id>
```
### Additional tips
- Combine flags (for example, `--checks` or `--services`) just like with other providers.
- Use `--output-modes` to export findings in JSON, CSV, ASFF, etc.
- Rotate API keys regularly and update the stored credentials in Prowler App to maintain connectivity.
For more examples (filters, outputs, scheduling), refer back to the [MongoDB Atlas documentation hub](./authentication.mdx) and the main Prowler CLI usage guide.
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
PROWLER_APP_API_KEY="pk_your_api_key_here"
PROWLER_API_BASE_URL="https://api.prowler.com"
API_BASE_URL="https://api.prowler.com/api/v1"
PROWLER_MCP_TRANSPORT_MODE="stdio"
+310
View File
@@ -0,0 +1,310 @@
# Prowler MCP Server - AI Agent Ruleset
**Complete guide for AI agents and developers working on the Prowler MCP Server - the Model Context Protocol server that provides AI agents access to the Prowler ecosystem.**
## Project Overview
The Prowler MCP Server brings the entire Prowler ecosystem to AI assistants through
the Model Context Protocol (MCP). It enables seamless integration with AI tools
like Claude Desktop, Cursor, and other MCP hosts, allowing interaction with
Prowler's security capabilities through natural language.
---
## Critical Rules
### Tool Implementation
- **ALWAYS**: Extend `BaseTool` ABC for new Prowler App tools (auto-registration)
- **ALWAYS**: Use `@mcp.tool()` decorator for Hub/Docs tools (manual registration)
- **NEVER**: Manually register BaseTool subclasses (auto-discovered via `load_all_tools()`)
- **NEVER**: Import tools directly in server.py (tool_loader handles discovery)
### Models
- **ALWAYS**: Use `MinimalSerializerMixin` for LLM-optimized responses
- **ALWAYS**: Implement `from_api_response()` factory method for API transformations
- **ALWAYS**: Use two-tier models (Simplified for lists, Detailed for single items)
- **NEVER**: Return raw API responses (transform to simplified models)
### API Client
- **ALWAYS**: Use singleton `ProwlerAPIClient` via `self.api_client` in tools
- **ALWAYS**: Use `build_filter_params()` for query parameter normalization
- **NEVER**: Create new httpx clients in tools (use shared client)
---
## Architecture
### Three Sub-Servers Pattern
The main server (`server.py`) orchestrates three independent sub-servers with prefixed tool namespacing:
```python
# server.py imports sub-servers with prefixes
await prowler_mcp_server.import_server(hub_mcp_server, prefix="prowler_hub")
await prowler_mcp_server.import_server(app_mcp_server, prefix="prowler_app")
await prowler_mcp_server.import_server(docs_mcp_server, prefix="prowler_docs")
```
This pattern ensures:
- Failures in one sub-server do not block others
- Clear tool namespacing for LLM disambiguation
- Independent development and testing
### Tool Naming Convention
All tools follow a consistent naming pattern with prefixes:
- `prowler_hub_*` - Prowler Hub catalog and compliance tools
- `prowler_docs_*` - Prowler documentation search and retrieval
- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
### Tool Registration Patterns
**Pattern 1: Prowler Hub/Docs (Direct Decorators)**
```python
# prowler_hub/server.py or prowler_documentation/server.py
hub_mcp_server = FastMCP("prowler-hub")
@hub_mcp_server.tool()
async def get_checks(providers: str | None = None) -> dict:
"""Tool docstring becomes LLM description."""
# Direct implementation
response = prowler_hub_client.get("/check", params=params)
return response.json()
```
**Pattern 2: Prowler App (BaseTool Auto-Registration)**
```python
# prowler_app/tools/findings.py
class FindingsTools(BaseTool):
async def search_security_findings(
self,
severity: list[str] = Field(default=[], description="Filter by severity")
) -> dict:
"""Docstring becomes LLM description."""
response = await self.api_client.get("/api/v1/findings")
return SimplifiedFinding.from_api_response(response).model_dump()
```
NOTE: Only public methods of `BaseTool` subclasses are registered as tools.
---
## Tech Stack
- **Language**: Python 3.12+
- **MCP Framework**: FastMCP 2.13.1
- **HTTP Client**: httpx (async)
- **Validation**: Pydantic with MinimalSerializerMixin
- **Package Manager**: uv
---
## Project Structure
```
mcp_server/
├── README.md # User documentation
├── AGENTS.md # This file - AI agent guidelines
├── CHANGELOG.md # Version history
├── pyproject.toml # Project metadata and dependencies
├── Dockerfile # Container image definition
├── entrypoint.sh # Docker entrypoint script
└── prowler_mcp_server/
├── __init__.py # Version info
├── main.py # CLI entry point
├── server.py # Main FastMCP server orchestration
├── lib/
│ └── logger.py # Structured logging
├── prowler_hub/
│ └── server.py # Hub tools (10 tools, no auth)
├── prowler_app/
│ ├── server.py # App server initialization
│ ├── tools/
│ │ ├── base.py # BaseTool abstract class
│ │ ├── findings.py # Findings tools
│ │ ├── providers.py # Provider tools
│ │ ├── scans.py # Scan tools
│ │ ├── resources.py # Resource tools
│ │ └── muting.py # Muting tools
│ ├── models/
│ │ ├── base.py # MinimalSerializerMixin
│ │ ├── findings.py # Finding models
│ │ ├── providers.py # Provider models
│ │ ├── scans.py # Scan models
│ │ ├── resources.py # Resource models
│ │ └── muting.py # Muting models
│ └── utils/
│ ├── api_client.py # ProwlerAPIClient singleton
│ ├── auth.py # ProwlerAppAuth (STDIO/HTTP)
│ └── tool_loader.py # Auto-discovery and registration
└── prowler_documentation/
├── server.py # Documentation tools (2 tools, no auth)
└── search_engine.py # Mintlify API integration
```
---
## Commands
NOTE: To run a python command always use `uv run <command>` from within the `mcp_server/` directory.
### Development
```bash
# Navigate to MCP server directory
cd mcp_server
# Run in STDIO mode (default)
uv run prowler-mcp
# Run in HTTP mode
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8000
# Run from anywhere using uvx
uvx /path/to/prowler/mcp_server/
```
---
## Development Patterns
### Adding New Tools to Prowler App
1. **Create or extend a tool class** in `prowler_app/tools/`:
```python
# prowler_app/tools/new_feature.py
from pydantic import Field
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from prowler_mcp_server.prowler_app.models.new_feature import FeatureResponse
class NewFeatureTools(BaseTool):
async def list_features(
self,
status: str | None = Field(default=None, description="Filter by status")
) -> dict:
"""List all features with optional filtering.
Returns a simplified list of features optimized for LLM consumption.
"""
params = {}
if status:
params["filter[status]"] = status
clean_params = self.api_client.build_filter_params(params)
response = await self.api_client.get("/api/v1/features", params=clean_params)
return FeatureResponse.from_api_response(response).model_dump()
```
2. **Create corresponding models** in `prowler_app/models/`:
```python
# prowler_app/models/new_feature.py
from pydantic import Field
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
class SimplifiedFeature(MinimalSerializerMixin):
"""Lightweight feature for list operations."""
id: str
name: str
status: str
class DetailedFeature(SimplifiedFeature):
"""Extended feature with complete details."""
description: str | None = None
created_at: str
updated_at: str
@classmethod
def from_api_response(cls, data: dict) -> "DetailedFeature":
"""Transform API response to model."""
attributes = data.get("attributes", {})
return cls(
id=data["id"],
name=attributes["name"],
status=attributes["status"],
description=attributes.get("description"),
created_at=attributes["created_at"],
updated_at=attributes["updated_at"],
)
```
3. **No registration needed** - the tool loader auto-discovers BaseTool subclasses
### Adding Tools to Prowler Hub/Docs
Use the `@mcp.tool()` decorator directly:
```python
# prowler_hub/server.py
@hub_mcp_server.tool()
async def new_hub_tool(param: str) -> dict:
"""Tool description for LLM."""
response = prowler_hub_client.get("/endpoint")
return response.json()
```
---
## Code Quality Standards
### Tool Docstrings
Tool docstrings become AI agent descriptions. Write them in a clear, concise manner focusing on LLM-relevant behavior:
```python
async def search_security_findings(
self,
severity: list[str] = Field(default=[], description="Filter by severity levels")
) -> dict:
"""Search security findings with advanced filtering.
Returns a lightweight list of findings optimized for LLM consumption.
Use get_finding_details for complete information about a specific finding.
"""
```
### Model Design
- Use `MinimalSerializerMixin` to exclude None/empty values
- Implement `from_api_response()` for consistent API transformation
- Create two-tier models: Simplified (lists) and Detailed (single items)
### Error Handling
Return structured error responses rather than raising exceptions:
```python
try:
response = await self.api_client.get(f"/api/v1/items/{item_id}")
return DetailedItem.from_api_response(response["data"]).model_dump()
except Exception as e:
self.logger.error(f"Failed to get item {item_id}: {e}")
return {"error": str(e), "status": "failed"}
```
---
## QA Checklist Before Commit
- [ ] Tool docstrings are clear and describe LLM-relevant behavior
- [ ] Models use `MinimalSerializerMixin` for LLM optimization
- [ ] API responses are transformed to simplified models
- [ ] No hardcoded secrets or API keys
- [ ] Error handling returns structured responses
- [ ] New tools are auto-discovered (BaseTool subclass) or properly decorated
- [ ] Parameter descriptions use Pydantic `Field()` with clear descriptions
---
## References
- **Root Project Guide**: `../AGENTS.md`
- **FastMCP Documentation**: https://gofastmcp.com/llms.txt
- **Prowler API Documentation**: https://api.prowler.com/api/v1/docs
+6
View File
@@ -2,6 +2,12 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.2.1] (UNRELEASED)
### Changed
- Update API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9300)
## [0.2.0] (Prowler v5.15.0)
### Added
+121 -427
View File
@@ -1,18 +1,59 @@
# Prowler MCP Server
> ⚠️ **Preview Feature**: This MCP server is currently in preview and under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack) to discuss and share your thoughts.
**Prowler MCP Server** brings the entire Prowler ecosystem to AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). It enables seamless integration with AI tools like Claude Desktop, Cursor, and other MCP clients, allowing interaction with Prowler's security capabilities through natural language.
Access the entire Prowler ecosystem through the Model Context Protocol (MCP). This server provides three main capabilities:
> **Preview Feature**: This MCP server is currently under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack).
- **Prowler Cloud and Prowler App (Self-Managed)**: Full access to Prowler Cloud platform and Prowler Self-Managed for managing providers, running scans, and analyzing security findings
- **Prowler Hub**: Access to Prowler's security checks, fixers, and compliance frameworks catalog
- **Prowler Documentation**: Search and retrieve official Prowler documentation
## Key Capabilities
## Quick Start with Hosted Server (Recommended)
### Prowler Cloud and Prowler App (Self-Managed)
**The easiest way to use Prowler MCP is through our hosted server at `https://mcp.prowler.com/mcp`**
Full access to Prowler Cloud platform and self-managed Prowler App for:
- **Findings Analysis**: Query, filter, and analyze security findings across all your cloud environments
- **Provider Management**: Create, configure, and manage your configured Prowler providers (AWS, Azure, GCP, etc.)
- **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting rules to suppress non-critical findings
No installation required! Just configure your MCP client:
### Prowler Hub
Access to Prowler's comprehensive security knowledge base:
- **Security Checks Catalog**: Browse and search **over 1000 security checks** across multiple Prowler providers
- **Check Implementation**: View the Python code that powers each security check
- **Automated Fixers**: Access remediation scripts for common security issues
- **Compliance Frameworks**: Explore mappings to **over 70 compliance standards and frameworks**
- **Provider Services**: View available services and checks for each cloud provider
### Prowler Documentation
Search and retrieve official Prowler documentation:
- **Intelligent Search**: Full-text search across all Prowler documentation
- **Contextual Results**: Get relevant documentation pages with highlighted snippets
- **Document Retrieval**: Access complete markdown content of any documentation file
## Documentation
For comprehensive guides and tutorials, see the official documentation:
| Guide | Description |
|-------|-------------|
| [Overview](https://docs.prowler.com/getting-started/products/prowler-mcp) | Key capabilities, use cases, and deployment options |
| [Installation](https://docs.prowler.com/getting-started/installation/prowler-mcp) | Docker, PyPI, and source installation |
| [Configuration](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp) | Configure Claude Desktop, Cursor, and other MCP clients |
| [Tools Reference](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools) | Complete reference of all tools |
| [Developer Guide](https://docs.prowler.com/developer-guide/mcp-server) | How to extend with new tools |
## Deployment Options
Prowler MCP Server can be used in three ways:
### 1. Prowler Cloud MCP Server (Recommended)
**Use Prowler's managed MCP server at `https://mcp.prowler.com/mcp`**
- No installation required
- Managed and maintained by Prowler team
- Always up-to-date
```json
{
@@ -30,70 +71,37 @@ No installation required! Just configure your MCP client:
}
```
**Configuration file locations:**
- **Claude Desktop (macOS)**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Claude Desktop (Windows)**: `%AppData%\Claude\claude_desktop_config.json`
- **Cursor**: `~/.cursor/mcp.json`
### 2. Local STDIO Mode
Get your API key at [Prowler Cloud](https://cloud.prowler.com) → Settings → API Keys
**Run the server locally on your machine**
> **Benefits:** Always up-to-date, no maintenance, managed by Prowler team
- Runs as a subprocess of your MCP client
- Requires Python 3.12+ or Docker
## Local/Self-Hosted Installation
### 3. Self-Hosted HTTP Mode
If you need to run the MCP server locally or self-host it, choose one of the following installation methods. **Configuration is the same** for both managed and local installations - just point to your local server URL instead of `https://mcp.prowler.com/mcp`.
**Deploy your own remote MCP server**
### Requirements
- Full control over deployment
- Requires Python 3.12+ or Docker
- Python 3.12+ (for source/PyPI installation)
- Docker (for Docker installation)
- Network access to `https://hub.prowler.com` (for Prowler Hub)
- Network access to `https://prowler.mintlify.app` (for Prowler Documentation)
- Network access to Prowler Cloud and Prowler App (Self-Managed) API (optional, only for Prowler Cloud/App features)
- Prowler Cloud account credentials (only for Prowler Cloud and Prowler App features)
See the [Installation Guide](https://docs.prowler.com/getting-started/installation/prowler-mcp) for complete instructions.
### Installation Methods
## Quick Installation
#### Option 1: Docker Hub (Recommended)
Pull the official image from Docker Hub:
### Docker (Recommended)
```bash
docker pull prowlercloud/prowler-mcp
```
Run in STDIO mode:
```bash
# STDIO mode
docker run --rm -i prowlercloud/prowler-mcp
# HTTP mode
docker run --rm -p 8000:8000 prowlercloud/prowler-mcp --transport http --host 0.0.0.0 --port 8000
```
Run in HTTP mode:
```bash
docker run --rm -p 8000:8000 \
prowlercloud/prowler-mcp \
--transport http --host 0.0.0.0 --port 8000
```
With environment variables:
```bash
docker run --rm -i \
-e PROWLER_APP_API_KEY="pk_your_api_key" \
-e PROWLER_API_BASE_URL="https://api.prowler.com" \
prowlercloud/prowler-mcp
```
**Docker Hub:** [prowlercloud/prowler-mcp](https://hub.docker.com/r/prowlercloud/prowler-mcp)
#### Option 2: PyPI Package (Coming Soon)
```bash
pip install prowler-mcp-server
prowler-mcp --help
```
#### Option 3: From Source (Development)
Clone the repository and use `uv`:
### From Source
```bash
git clone https://github.com/prowler-cloud/prowler.git
@@ -101,400 +109,86 @@ cd prowler/mcp_server
uv run prowler-mcp --help
```
Install [uv](https://docs.astral.sh/uv/) first if needed.
#### Option 4: Build Docker Image from Source
```bash
git clone https://github.com/prowler-cloud/prowler.git
cd prowler/mcp_server
docker build -t prowler-mcp .
docker run --rm -i prowler-mcp
```
## Running
The Prowler MCP server supports two transport modes:
- **STDIO mode** (default): For direct integration with MCP clients like Claude Desktop
- **HTTP mode**: For remote access over HTTP with Bearer token authentication
### Transport Modes
#### STDIO Mode (Default)
STDIO mode is the standard MCP transport for direct client integration:
```bash
cd prowler/mcp_server
uv run prowler-mcp
# or
uv run prowler-mcp --transport stdio
```
#### HTTP Mode (Remote Server)
HTTP mode allows the server to run as a remote service accessible over HTTP:
```bash
cd prowler/mcp_server
# Run on default host and port (127.0.0.1:8000)
uv run prowler-mcp --transport http
# Run on custom host and port
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8080
```
For self-deployed MCP remote server, you can use also configure the server to use a custom API base URL with the environment variable `PROWLER_API_BASE_URL`; and the transport mode with the environment variable `PROWLER_MCP_TRANSPORT_MODE`.
```bash
export PROWLER_API_BASE_URL="https://api.prowler.com"
export PROWLER_MCP_TRANSPORT_MODE="http"
```
### Using uv directly
After installation, start the MCP server via the console script:
```bash
cd prowler/mcp_server
uv run prowler-mcp
```
Alternatively, you can run from wherever you want using `uvx` command:
```bash
uvx /path/to/prowler/mcp_server/
```
### Using Docker
#### STDIO Mode (Default)
Run the pre-built Docker container in STDIO mode:
```bash
cd prowler/mcp_server
docker run --rm --env-file ./.env -it prowler-mcp
```
#### HTTP Mode (Remote Server)
Run as a remote HTTP server:
```bash
cd prowler/mcp_server
# Run on port 8000 (accessible from host)
docker run --rm --env-file ./.env -p 8000:8000 -it prowler-mcp --transport http --host 0.0.0.0 --port 8000
# Run on custom port
docker run --rm --env-file ./.env -p 8080:8080 -it prowler-mcp --transport http --host 0.0.0.0 --port 8080
```
## Production Deployment
For production deployments that require customization, it is recommended to use the ASGI application that can be found in `prowler_mcp_server.server`. This can be run with uvicorn:
```bash
uvicorn prowler_mcp_server.server:app --host 0.0.0.0 --port 8000
```
For more details on production deployment options, see the [FastMCP production deployment guide](https://gofastmcp.com/deployment/http#production-deployment) and [uvicorn settings](https://www.uvicorn.org/settings/).
## Command Line Arguments
The Prowler MCP server supports the following command line arguments:
```
prowler-mcp [--transport {stdio,http}] [--host HOST] [--port PORT]
```
**Arguments:**
- `--transport {stdio,http}`: Transport method (default: stdio)
- `stdio`: Standard input/output transport for direct MCP client integration
- `http`: HTTP transport for remote server access
- `--host HOST`: Host to bind to for HTTP transport (default: 127.0.0.1)
- `--port PORT`: Port to bind to for HTTP transport (default: 8000)
**Examples:**
```bash
# Default STDIO mode
prowler-mcp
# Explicit STDIO mode
prowler-mcp --transport stdio
# HTTP mode with default host and port (127.0.0.1:8000)
prowler-mcp --transport http
# HTTP mode accessible from any network interface
prowler-mcp --transport http --host 0.0.0.0
# HTTP mode with custom port
prowler-mcp --transport http --host 0.0.0.0 --port 8080
```
## Available Tools
### Prowler Hub
For complete tool descriptions and parameters, see the [Tools Reference](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools).
All tools are exposed under the `prowler_hub` prefix.
### Tool Naming Convention
- `prowler_hub_get_check_filters`: Return available filter values for checks (providers, services, severities, categories, compliances). Call this before `prowler_hub_get_checks` to build valid queries.
- `prowler_hub_get_checks`: List checks with option of advanced filtering.
- `prowler_hub_get_check_raw_metadata`: Fetch raw check metadata JSON (low-level version of get_checks).
- `prowler_hub_get_check_code`: Fetch check implementation Python code from Prowler.
- `prowler_hub_get_check_fixer`: Fetch check fixer Python code from Prowler (if it exists).
- `prowler_hub_search_checks`: Fulltext search across check metadata.
- `prowler_hub_get_compliance_frameworks`: List/filter compliance frameworks.
- `prowler_hub_search_compliance_frameworks`: Full-text search across frameworks.
- `prowler_hub_list_providers`: List Prowler official providers and their services.
- `prowler_hub_get_artifacts_count`: Return total artifact count (checks + frameworks).
All tools follow a consistent naming pattern with prefixes:
- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
- `prowler_hub_*` - Prowler Hub catalog and compliance tools
- `prowler_docs_*` - Prowler documentation search and retrieval
### Prowler Documentation
## Architecture
All tools are exposed under the `prowler_docs` prefix.
- `prowler_docs_search`: Search the official Prowler documentation using fulltext search. Returns relevant documentation pages with highlighted snippets and relevance scores.
- `prowler_docs_get_document`: Retrieve the full markdown content of a specific documentation file using the path from search results.
### Prowler Cloud and Prowler App (Self-Managed)
All tools are exposed under the `prowler_app` prefix.
#### Findings Management
- `prowler_app_list_findings`: List security findings from Prowler scans with advanced filtering
- `prowler_app_get_finding`: Get detailed information about a specific security finding
- `prowler_app_get_latest_findings`: Retrieve latest findings from the latest scans for each provider
- `prowler_app_get_findings_metadata`: Fetch unique metadata values from filtered findings
- `prowler_app_get_latest_findings_metadata`: Fetch metadata from latest findings across all providers
#### Provider Management
- `prowler_app_list_providers`: List all providers with filtering options
- `prowler_app_create_provider`: Create a new provider in the current tenant
- `prowler_app_get_provider`: Get detailed information about a specific provider
- `prowler_app_update_provider`: Update provider details (alias, etc.)
- `prowler_app_delete_provider`: Delete a specific provider
- `prowler_app_test_provider_connection`: Test provider connection status
#### Provider Secrets Management
- `prowler_app_list_provider_secrets`: List all provider secrets with filtering
- `prowler_app_add_provider_secret`: Add or update credentials for a provider
- `prowler_app_get_provider_secret`: Get detailed information about a provider secret
- `prowler_app_update_provider_secret`: Update provider secret details
- `prowler_app_delete_provider_secret`: Delete a provider secret
#### Scan Management
- `prowler_app_list_scans`: List all scans with filtering options
- `prowler_app_create_scan`: Trigger a manual scan for a specific provider
- `prowler_app_get_scan`: Get detailed information about a specific scan
- `prowler_app_update_scan`: Update scan details
- `prowler_app_get_scan_compliance_report`: Download compliance report as CSV
- `prowler_app_get_scan_report`: Download ZIP file containing scan report
#### Schedule Management
- `prowler_app_schedules_daily_scan`: Create a daily scheduled scan for a provider
#### Processor Management
- `prowler_app_processors_list`: List all processors with filtering
- `prowler_app_processors_create`: Create a new processor. For now, only mute lists are supported.
- `prowler_app_processors_retrieve`: Get processor details by ID
- `prowler_app_processors_partial_update`: Update processor configuration
- `prowler_app_processors_destroy`: Delete a processor
## Configuration
### Prowler Cloud and Prowler App (Self-Managed) Authentication
> [!IMPORTANT]
> Authentication is not needed for using Prowler Hub or Prowler Documentation features.
The Prowler MCP server supports different authentication in Prowler Cloud and Prowler App (Self-Managed) methods depending on the transport mode:
#### STDIO Mode Authentication
For STDIO mode, authentication is handled via environment variables using an API key:
```bash
# Required for Prowler Cloud and Prowler App (Self-Managed) authentication
export PROWLER_APP_API_KEY="pk_your_api_key_here"
# Optional - for custom API endpoint, in case not provided Prowler Cloud API will be used
export PROWLER_API_BASE_URL="https://api.prowler.com"
```
prowler_mcp_server/
├── server.py # Main orchestrator (imports sub-servers with prefixes)
├── main.py # CLI entry point
├── prowler_hub/ # tools - no authentication required
├── prowler_app/ # tools - authentication required
│ ├── tools/ # Tool implementations
│ ├── models/ # Pydantic models for LLM-optimized responses
│ └── utils/ # API client, authentication, tool loader
└── prowler_documentation/ # tools - no authentication required
```
#### HTTP Mode Authentication
**Key Features:**
- **Modular Design**: Three independent sub-servers with prefixed namespacing
- **Auto-Discovery**: Prowler App tools are automatically discovered and registered
- **LLM Optimization**: Response models minimize token usage by excluding empty values
- **Dual Transport**: Supports both STDIO (local) and HTTP (remote) modes
For HTTP mode (remote server), authentication is handled via Bearer tokens. The MCP server supports both JWT tokens and API keys:
## Use Cases
**Option 1: Using API Keys (Recommended)**
Use your Prowler API key directly in the MCP client configuration with Bearer token format:
```
Authorization: Bearer pk_your_api_key_here
```
The Prowler MCP Server enables powerful workflows through AI assistants:
**Option 2: Using JWT Tokens**
You need to obtain a JWT token from Prowler Cloud/App and include the generated token in the MCP client configuration. To get a valid token, you can use the following command (replace the email and password with your own credentials):
**Security Operations**
- "Show me all critical findings from my AWS production accounts"
- "Register my new AWS account in Prowler and run a scheduled scan every day"
- "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity"
```bash
curl -X POST https://api.prowler.com/api/v1/tokens \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-d '{
"data": {
"type": "tokens",
"attributes": {
"email": "your-email@example.com",
"password": "your-password"
}
}
}'
```
**Security Research**
- "Explain what the S3 bucket public access Prowler check does"
- "Find all Prowler checks related to encryption at rest"
- "What is the latest version of the CIS that Prowler is covering per provider?"
The response will be a JWT token that you can use to [authenticate your MCP client](#http-mode-configuration-remote-server).
**Documentation & Learning**
- "How do I configure Prowler to scan my GCP organization?"
- "What authentication methods does Prowler support for Azure?"
- "How can I contribute with a new security check to Prowler?"
### MCP Client Configuration
## Requirements
Configure your MCP client, like Claude Desktop, Cursor, etc, to connect to the server. The configuration depends on whether you're running in STDIO mode (local) or HTTP mode (remote).
**For Prowler Cloud MCP Server:**
- Prowler Cloud account and API key (only for Prowler Cloud/App features)
#### STDIO Mode Configuration
**For self-hosted STDIO/HTTP Mode:**
- Python 3.12+ or Docker
- Network access to:
- `https://hub.prowler.com` (for Prowler Hub)
- `https://docs.prowler.com` (for Prowler Documentation)
- Prowler Cloud API or self-hosted Prowler App API (for Prowler Cloud/App features)
For local execution, configure your MCP client to launch the server directly. Below are examples for both direct execution and Docker deployment; consult your client's documentation for exact locations.
> **No Authentication Required**: Prowler Hub and Prowler Documentation features work without authentication. A Prowler API key is only required to access Prowler Cloud or Prowler App (Self-Managed) features.
##### Using uvx (Direct Execution)
## Configuring MCP Hosts
```json
{
"mcpServers": {
"prowler": {
"command": "uvx",
"args": ["/path/to/prowler/mcp_server/"],
"env": {
"PROWLER_APP_API_KEY": "pk_your_api_key_here",
"PROWLER_API_BASE_URL": "https://api.prowler.com" // Optional, in case not provided Prowler Cloud API will be used
}
}
}
}
```
To configure your MCP host (Claude Code, Cursor, etc.) see the [Configuration Guide](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp) for detailed setup instructions.
##### Using Docker
## Contributing
```json
{
"mcpServers": {
"prowler": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"--env", "PROWLER_APP_API_KEY=pk_your_api_key_here",
"--env", "PROWLER_API_BASE_URL=https://api.prowler.com", // Optional, in case not provided Prowler Cloud API will be used
"prowler-mcp"
]
}
}
}
```
For developers looking to extend the MCP server with new tools or features:
#### HTTP Mode Configuration (Remote Server)
- **[Developer Guide](https://docs.prowler.com/developer-guide/mcp-server)**: Step-by-step instructions for adding new tools
- **[AGENTS.md](./AGENTS.md)**: AI agent guidelines and coding patterns
For HTTP mode, you can configure your MCP client to connect to a remote Prowler MCP server.
## Related Products
Most MCP clients don't natively support HTTP transport with Bearer token authentication. However, you can use the `mcp-remote` proxy tool to connect any MCP client to remote HTTP servers.
##### Using mcp-remote Proxy (Recommended for Claude Desktop)
For clients like Claude Desktop that don't support HTTP transport natively, use the `mcp-remote` npm package as a proxy:
**Using API Key (Recommended):**
```json
{
"mcpServers": {
"prowler": {
"command": "npx",
"args": [
"mcp-remote",
"https://mcp.prowler.com/mcp",
"--header",
"Authorization: Bearer pk_your_api_key_here"
]
}
}
}
```
**Using JWT Token:**
```json
{
"mcpServers": {
"prowler": {
"command": "npx",
"args": [
"mcp-remote",
"https://mcp.prowler.com/mcp",
"--header",
"Authorization: Bearer <your-jwt-token-here>"
]
}
}
}
```
> **Note:** Replace `https://mcp.prowler.com/mcp` with your actual MCP server URL (use `http://localhost:8000/mcp` for local deployment). The `mcp-remote` package is automatically installed by `npx` on first use.
> **Info:** The `mcp-remote` tool acts as a bridge, converting STDIO protocol (used by Claude Desktop) to HTTP requests (used by the remote MCP server). Learn more at [mcp-remote on npm](https://www.npmjs.com/package/mcp-remote).
##### Direct HTTP Configuration (For Compatible Clients)
For clients that natively support HTTP transport with Bearer token authentication:
**Using API Key:**
```json
{
"mcpServers": {
"prowler": {
"url": "https://mcp.prowler.com/mcp",
"headers": {
"Authorization": "Bearer pk_your_api_key_here"
}
}
}
}
```
**Using JWT Token:**
```json
{
"mcpServers": {
"prowler": {
"url": "https://mcp.prowler.com/mcp",
"headers": {
"Authorization": "Bearer <your-jwt-token-here>"
}
}
}
}
```
> **Note:** Replace `mcp.prowler.com` with your actual server hostname and adjust the port if needed (e.g., `http://localhost:8000/mcp` for local deployment).
### Claude Desktop (macOS/Windows)
Add the example server to Claude Desktop's config file, then restart the app.
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%AppData%\Claude\claude_desktop_config.json` (e.g. `C:\\Users\\<you>\\AppData\\Roaming\\Claude\\claude_desktop_config.json`)
### Cursor (macOS/Linux)
If you want to have it globally available, add the example server to Cursor's config file, then restart the app.
- macOS/Linux: `~/.cursor/mcp.json`
If you want to have it only for the current project, add the example server to the project's root in a new `.cursor/mcp.json` file.
## Documentation
For detailed documentation about the Prowler MCP Server, including guides, tutorials, and use cases, visit the [official Prowler documentation](https://docs.prowler.com).
- **[Prowler Hub](https://hub.prowler.com)**: Browse security checks and compliance frameworks
- **[Prowler Cloud](https://cloud.prowler.com)**: Managed Prowler platform
- **[Lighthouse AI](https://docs.prowler.com/getting-started/products/prowler-lighthouse-ai)**: AI security analyst
## License
@@ -33,7 +33,7 @@ class BaseTool(ABC):
async def search_security_findings(self, severity: list[str] = Field(...)):
# Implementation with access to self.api_client
response = await self.api_client.get("/api/v1/findings")
response = await self.api_client.get("/findings")
return response
"""
@@ -6,13 +6,14 @@ across all cloud providers.
from typing import Any, Literal
from pydantic import Field
from prowler_mcp_server.prowler_app.models.findings import (
DetailedFinding,
FindingsListResponse,
FindingsOverview,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class FindingsTools(BaseTool):
@@ -122,11 +123,11 @@ class FindingsTools(BaseTool):
if date_range is None:
# No dates provided - use latest findings endpoint
endpoint = "/api/v1/findings/latest"
endpoint = "/findings/latest"
params = {}
else:
# Dates provided - use historical findings endpoint
endpoint = "/api/v1/findings"
endpoint = "/findings"
params = {
"filter[inserted_at__gte]": date_range[0],
"filter[inserted_at__lte]": date_range[1],
@@ -228,7 +229,7 @@ class FindingsTools(BaseTool):
# Get API response and transform to detailed format
api_response = await self.api_client.get(
f"/api/v1/findings/{finding_id}", params=params
f"/findings/{finding_id}", params=params
)
detailed_finding = DetailedFinding.from_api_response(
api_response.get("data", {})
@@ -281,7 +282,7 @@ class FindingsTools(BaseTool):
# Get API response and transform to simplified format
api_response = await self.api_client.get(
"/api/v1/overviews/findings", params=clean_params
"/overviews/findings", params=clean_params
)
overview = FindingsOverview.from_api_response(api_response)
@@ -8,13 +8,14 @@ This module provides tools for managing finding muting in Prowler, including:
import json
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.muting import (
DetailedMuteRule,
MutelistResponse,
MuteRulesListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class MutingTools(BaseTool):
@@ -53,9 +54,7 @@ class MutingTools(BaseTool):
}
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/processors", params=clean_params
)
api_response = await self.api_client.get("/processors", params=clean_params)
data = api_response.get("data", [])
@@ -145,7 +144,7 @@ Structure:
}
api_response = await self.api_client.post(
"/api/v1/processors", json_data=create_body
"/processors", json_data=create_body
)
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
return mutelist.model_dump()
@@ -163,7 +162,7 @@ Structure:
}
api_response = await self.api_client.patch(
f"/api/v1/processors/{existing_mutelist['id']}", json_data=update_body
f"/processors/{existing_mutelist['id']}", json_data=update_body
)
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
return mutelist.model_dump()
@@ -194,7 +193,7 @@ Structure:
# Delete the mutelist
mutelist_id = existing_mutelist["id"]
await self.api_client.delete(f"/api/v1/processors/{mutelist_id}")
await self.api_client.delete(f"/processors/{mutelist_id}")
return {
"success": True,
@@ -276,9 +275,7 @@ Structure:
params["filter[search]"] = search
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/mute-rules", params=clean_params
)
api_response = await self.api_client.get("/mute-rules", params=clean_params)
simplified_response = MuteRulesListResponse.from_api_response(api_response)
return simplified_response.model_dump()
@@ -311,7 +308,7 @@ Structure:
}
api_response = await self.api_client.get(
f"/api/v1/mute-rules/{rule_id}", params=params
f"/mute-rules/{rule_id}", params=params
)
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
@@ -363,9 +360,7 @@ Structure:
}
}
api_response = await self.api_client.post(
"/api/v1/mute-rules", json_data=create_body
)
api_response = await self.api_client.post("/mute-rules", json_data=create_body)
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
@@ -432,10 +427,9 @@ Structure:
}
api_response = await self.api_client.patch(
f"/api/v1/mute-rules/{rule_id}", json_data=update_body
f"/mute-rules/{rule_id}", json_data=update_body
)
self.logger.info(f"API response: {api_response}")
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
@@ -463,7 +457,7 @@ Structure:
"""
self.logger.info(f"Deleting mute rule {rule_id}...")
result = await self.api_client.delete(f"/api/v1/mute-rules/{rule_id}")
result = await self.api_client.delete(f"/mute-rules/{rule_id}")
if result.get("success"):
return {
@@ -6,12 +6,13 @@ including searching, connecting, and deleting providers.
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.providers import (
ProviderConnectionStatus,
ProvidersListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ProvidersTools(BaseTool):
@@ -100,9 +101,7 @@ class ProvidersTools(BaseTool):
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/providers", params=clean_params
)
api_response = await self.api_client.get("/providers", params=clean_params)
simplified_response = ProvidersListResponse.from_api_response(api_response)
# Fetch secret_type for each provider that has a secret
@@ -306,9 +305,7 @@ class ProvidersTools(BaseTool):
self.logger.info(f"Deleting provider {provider_id}...")
try:
# Initiate the deletion task
task_response = await self.api_client.delete(
f"/api/v1/providers/{provider_id}"
)
task_response = await self.api_client.delete(f"/providers/{provider_id}")
task_id = task_response.get("data", {}).get("id")
# Poll until task completes (with 60 second timeout)
@@ -345,7 +342,7 @@ class ProvidersTools(BaseTool):
"""
self.logger.info(f"Checking if provider {provider_uid} exists...")
response = await self.api_client.get(
"/api/v1/providers", params={"filter[uid]": provider_uid}
"/providers", params={"filter[uid]": provider_uid}
)
providers = response.get("data", [])
@@ -391,7 +388,7 @@ class ProvidersTools(BaseTool):
if alias:
provider_body["data"]["attributes"]["alias"] = alias
await self.api_client.post("/api/v1/providers", json_data=provider_body)
await self.api_client.post("/providers", json_data=provider_body)
provider_id = await self._check_provider_exists(provider_uid)
if provider_id is None:
@@ -418,7 +415,7 @@ class ProvidersTools(BaseTool):
}
}
result = await self.api_client.patch(
f"/api/v1/providers/{prowler_provider_id}", json_data=update_body
f"/providers/{prowler_provider_id}", json_data=update_body
)
if result.get("data", {}).get("attributes", {}).get("alias") != alias:
raise Exception(f"Provider {prowler_provider_id} alias update failed")
@@ -450,7 +447,7 @@ class ProvidersTools(BaseTool):
"""
try:
response = await self.api_client.get(
"/api/v1/providers/secrets",
"/providers/secrets",
params={"filter[provider]": prowler_provider_id},
)
secrets = response.get("data", [])
@@ -481,7 +478,7 @@ class ProvidersTools(BaseTool):
"""
try:
response = await self.api_client.get(
f"/api/v1/providers/secrets/{secret_id}",
f"/providers/secrets/{secret_id}",
params={"fields[provider-secrets]": "secret_type"},
)
secret_type = (
@@ -536,7 +533,7 @@ class ProvidersTools(BaseTool):
}
try:
response = await self.api_client.patch(
f"/api/v1/providers/secrets/{existing_secret_id}",
f"/providers/secrets/{existing_secret_id}",
json_data=update_body,
)
self.logger.info("Credentials updated successfully")
@@ -567,7 +564,7 @@ class ProvidersTools(BaseTool):
try:
response = await self.api_client.post(
"/api/v1/providers/secrets", json_data=secret_body
"/providers/secrets", json_data=secret_body
)
self.logger.info("Credentials added successfully")
return response
@@ -588,7 +585,7 @@ class ProvidersTools(BaseTool):
try:
# Initiate the connection test task
task_response = await self.api_client.post(
f"/api/v1/providers/{prowler_provider_id}/connection", json_data={}
f"/providers/{prowler_provider_id}/connection", json_data={}
)
task_id = task_response.get("data", {}).get("id")
@@ -619,5 +616,5 @@ class ProvidersTools(BaseTool):
Provider data dictionary
"""
return await self.api_client.get(
f"/api/v1/providers/{prowler_provider_id}",
f"/providers/{prowler_provider_id}",
)
@@ -6,13 +6,14 @@ across all providers.
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.resources import (
DetailedResource,
ResourcesListResponse,
ResourcesMetadataResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ResourcesTools(BaseTool):
@@ -121,11 +122,11 @@ class ResourcesTools(BaseTool):
if date_range is None:
# No dates provided - use latest resources endpoint
endpoint = "/api/v1/resources/latest"
endpoint = "/resources/latest"
params = {}
else:
# Dates provided - use historical resources endpoint
endpoint = "/api/v1/resources"
endpoint = "/resources"
params = {
"filter[updated_at__gte]": date_range[0],
"filter[updated_at__lte]": date_range[1],
@@ -206,9 +207,8 @@ class ResourcesTools(BaseTool):
# Get API response and transform to detailed format
api_response = await self.api_client.get(
f"/api/v1/resources/{resource_id}", params=params
f"/resources/{resource_id}", params=params
)
self.logger.info(f"API response: {api_response}")
detailed_resource = DetailedResource.from_api_response(
api_response.get("data", {})
)
@@ -265,13 +265,13 @@ class ResourcesTools(BaseTool):
if date_range is None:
# No dates provided - use latest metadata endpoint
metadata_endpoint = "/api/v1/resources/metadata/latest"
list_endpoint = "/api/v1/resources/latest"
metadata_endpoint = "/resources/metadata/latest"
list_endpoint = "/resources/latest"
params = {}
else:
# Dates provided - use historical endpoints
metadata_endpoint = "/api/v1/resources/metadata"
list_endpoint = "/api/v1/resources"
metadata_endpoint = "/resources/metadata"
list_endpoint = "/resources"
params = {
"filter[updated_at__gte]": date_range[0],
"filter[updated_at__lte]": date_range[1],
@@ -5,6 +5,8 @@ This module provides tools for managing and monitoring Prowler security scans.
from typing import Any, Literal
from pydantic import Field
from prowler_mcp_server.prowler_app.models.scans import (
DetailedScan,
ScanCreationResult,
@@ -12,7 +14,6 @@ from prowler_mcp_server.prowler_app.models.scans import (
ScheduleCreationResult,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ScansTools(BaseTool):
@@ -119,7 +120,7 @@ class ScansTools(BaseTool):
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get("/api/v1/scans", params=clean_params)
api_response = await self.api_client.get("/scans", params=clean_params)
simplified_response = ScansListResponse.from_api_response(api_response)
return simplified_response.model_dump()
@@ -163,9 +164,7 @@ class ScansTools(BaseTool):
"fields[scans]": "name,trigger,state,progress,duration,unique_resource_count,started_at,completed_at,scheduled_at,next_scan_at,inserted_at"
}
api_response = await self.api_client.get(
f"/api/v1/scans/{scan_id}", params=params
)
api_response = await self.api_client.get(f"/scans/{scan_id}", params=params)
detailed_scan = DetailedScan.from_api_response(api_response["data"])
return detailed_scan.model_dump()
@@ -213,9 +212,7 @@ class ScansTools(BaseTool):
# Create scan (returns Task)
self.logger.info(f"Creating scan for provider {provider_id}")
task_response = await self.api_client.post(
"/api/v1/scans", json_data=request_data
)
task_response = await self.api_client.post("/scans", json_data=request_data)
scan_id = (
task_response.get("data", {})
@@ -228,7 +225,7 @@ class ScansTools(BaseTool):
raise Exception("No scan_id returned from scan creation")
self.logger.info(f"Scan created successfully: {scan_id}")
scan_response = await self.api_client.get(f"/api/v1/scans/{scan_id}")
scan_response = await self.api_client.get(f"/scans/{scan_id}")
scan_info = DetailedScan.from_api_response(scan_response["data"])
return ScanCreationResult(
@@ -273,7 +270,7 @@ class ScansTools(BaseTool):
"""
self.logger.info(f"Creating daily schedule for provider {provider_id}")
task_response = await self.api_client.post(
"/api/v1/schedules/daily",
"/schedules/daily",
json_data={
"data": {
"type": "daily-schedules",
@@ -316,7 +313,7 @@ class ScansTools(BaseTool):
2. Use this tool with the scan 'id' and new name
"""
api_response = await self.api_client.patch(
f"/api/v1/scans/{scan_id}",
f"/scans/{scan_id}",
json_data={
"data": {
"type": "scans",
@@ -228,7 +228,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
)
# Fetch current task state
response = await self.get(f"/api/v1/tasks/{task_id}")
response = await self.get(f"/tasks/{task_id}")
task_data = response.get("data", {})
task_attrs = task_data.get("attributes", {})
state = task_attrs.get("state")
@@ -15,7 +15,7 @@ class ProwlerAppAuth:
def __init__(
self,
mode: str = os.getenv("PROWLER_MCP_TRANSPORT_MODE", "stdio"),
base_url: str = os.getenv("PROWLER_API_BASE_URL", "https://api.prowler.com"),
base_url: str = os.getenv("API_BASE_URL", "https://api.prowler.com/api/v1"),
):
self.base_url = base_url.rstrip("/")
logger.info(f"Using Prowler App API base URL: {self.base_url}")
+34
View File
@@ -2,6 +2,40 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.16.0] (Prowler UNRELEASED)
### Added
- `privilege-escalation` and `ec2-imdsv1` categories for AWS checks [(#9536)](https://github.com/prowler-cloud/prowler/pull/9536)
- Supported IaC formats and scanner documentation for the IaC provider [(#9553)](https://github.com/prowler-cloud/prowler/pull/9553)
### Changed
- Update AWS Glue service metadata to new format [(#9258)](https://github.com/prowler-cloud/prowler/pull/9258)
- Update AWS Kafka service metadata to new format [(#9261)](https://github.com/prowler-cloud/prowler/pull/9261)
- Update AWS KMS service metadata to new format [(#9263)](https://github.com/prowler-cloud/prowler/pull/9263)
- Update AWS MemoryDB service metadata to new format [(#9266)](https://github.com/prowler-cloud/prowler/pull/9266)
- Update AWS Inspector v2 service metadata to new format [(#9260)](https://github.com/prowler-cloud/prowler/pull/9260)
- Update AWS Service Catalog service metadata to new format [(#9410)](https://github.com/prowler-cloud/prowler/pull/9410)
- Update AWS SNS service metadata to new format [(#9428)](https://github.com/prowler-cloud/prowler/pull/9428)
- Update AWS Trusted Advisor service metadata to new format [(#9435)](https://github.com/prowler-cloud/prowler/pull/9435)
---
## [5.15.2] (Prowler UNRELEASED)
### Fixed
- Fix typo `trustboundaries` category to `trust-boundaries` [(#9536)](https://github.com/prowler-cloud/prowler/pull/9536)
- Store MongoDB Atlas provider regions as lowercase [(#9554)](https://github.com/prowler-cloud/prowler/pull/9554)
---
## [5.15.1] (Prowler v5.15.1)
### Fixed
- Fix false negative in AWS `apigateway_restapi_logging_enabled` check by refining stage logging evaluation to ensure logging level is not set to "OFF" [(#9304)](https://github.com/prowler-cloud/prowler/pull/9304)
---
## [5.15.0] (Prowler v5.15.0)
### Added
+1 -1
View File
@@ -38,7 +38,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.15.0"
prowler_version = "5.16.0"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
@@ -123,7 +123,10 @@ class APIGateway(AWSService):
waf = stage["webAclArn"]
if "methodSettings" in stage:
for settings in stage["methodSettings"].values():
if settings.get("loggingLevel"):
if (
settings.get("loggingLevel")
and settings.get("loggingLevel", "") != "OFF"
):
logging = True
if settings.get("cachingEnabled"):
cache_enabled = True
@@ -28,7 +28,7 @@
},
"Categories": [
"gen-ai",
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -28,7 +28,7 @@
},
"Categories": [
"gen-ai",
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -26,7 +26,8 @@
}
},
"Categories": [
"internet-exposed"
"internet-exposed",
"ec2-imdsv1"
],
"DependsOn": [],
"RelatedTo": [],
@@ -25,7 +25,9 @@
"Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html#configuring-instance-metadata-options"
}
},
"Categories": [],
"Categories": [
"ec2-imdsv1"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -26,7 +26,7 @@
}
},
"Categories": [
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "glue_data_catalogs_connection_passwords_encryption_enabled",
"CheckTitle": "Check if Glue data catalog settings have encrypt connection password enabled.",
"CheckTitle": "Glue data catalog connection password is encrypted with a KMS key",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"Description": "Check if Glue data catalog settings have encrypt connection password enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"Description": "**AWS Glue Data Catalog** settings for **connection password encryption** are evaluated to confirm an AWS KMS key is configured to encrypt passwords stored in connection properties.",
"Risk": "Unencrypted connection passwords can be read from the catalog or responses, letting attackers or over-privileged users obtain database credentials. This jeopardizes confidentiality of linked data stores, enables unauthorized modifications, and can facilitate lateral movement across environments.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-security.html",
"https://docs.aws.amazon.com/glue/latest/dg/encrypt-connection-passwords.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue put-data-catalog-encryption-settings --data-catalog-encryption-settings ConnectionPasswordEncryption={ReturnConnectionPasswordEncrypted=True,AwsKmsKeyId=<ksm_key_arn>",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_37#cloudformation",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_37#terraform"
"CLI": "aws glue put-data-catalog-encryption-settings --data-catalog-encryption-settings '{\"ConnectionPasswordEncryption\":{\"ReturnConnectionPasswordEncrypted\":true,\"AwsKmsKeyId\":\"<kms_key_arn>\"}}'",
"NativeIaC": "```yaml\n# CloudFormation: enable Glue Data Catalog connection password encryption\nResources:\n <example_resource_name>:\n Type: AWS::Glue::DataCatalogEncryptionSettings\n Properties:\n DataCatalogEncryptionSettings:\n ConnectionPasswordEncryption:\n ReturnConnectionPasswordEncrypted: true # Critical: encrypts connection passwords\n KmsKeyId: <kms_key_arn> # Critical: KMS key used for encryption\n```",
"Other": "1. In the AWS Console, go to AWS Glue\n2. Click Settings (left menu)\n3. Under Data catalog settings, check Encrypt connection passwords\n4. Select your KMS key (symmetric CMK)\n5. Click Save",
"Terraform": "```hcl\n# Enable Glue Data Catalog connection password encryption\nresource \"aws_glue_data_catalog_encryption_settings\" \"<example_resource_name>\" {\n data_catalog_encryption_settings {\n # Critical: enables password encryption with a KMS key\n connection_password_encryption {\n return_connection_password_encrypted = true\n aws_kms_key_id = \"<kms_key_arn>\"\n }\n\n # Required block for this resource; keep minimal\n encryption_at_rest {\n catalog_encryption_mode = \"DISABLED\"\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "On the AWS Glue console, you can enable this option on the Data catalog settings page.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/encrypt-connection-passwords.html"
"Text": "Enable **connection password encryption** in the Data Catalog with a customer-managed KMS key.\n- Apply **least privilege** to the KMS key and Glue roles\n- Prefer keeping responses encrypted (`ReturnConnectionPasswordEncrypted`)\n- Rotate keys and monitor access for **defense in depth**",
"Url": "https://hub.prowler.com/check/glue_data_catalogs_connection_passwords_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,35 @@
{
"Provider": "aws",
"CheckID": "glue_data_catalogs_metadata_encryption_enabled",
"CheckTitle": "Check if Glue data catalog settings have metadata encryption enabled.",
"CheckTitle": "Glue Data Catalog metadata is encrypted with KMS",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"Description": "Check if Glue data catalog settings have metadata encryption enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"Description": "**AWS Glue Data Catalog** metadata is encrypted at rest when catalog settings use **SSE-KMS** with a KMS key.\n\nCatalogs that do not configure `SSE-KMS` for metadata are considered unencrypted.",
"Risk": "Unencrypted catalog metadata exposes schemas, partitions, and data locations, reducing **confidentiality**.\n\nAdversaries or over-privileged users can conduct **reconnaissance** and plan lateral movement; tampering with definitions can corrupt queries and results, impacting **integrity**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/encrypt-glue-data-catalog.html",
"https://docs.amazonaws.cn/en_us/athena/latest/ug/encryption.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/data-catalog-encryption-at-rest-with-cmk.html",
"https://support.icompaas.com/support/solutions/articles/62000233381-ensure-glue-data-catalogs-are-not-publicly-accessible-"
],
"Remediation": {
"Code": {
"CLI": "aws glue put-data-catalog-encryption-settings --data-catalog-encryption-settings EncryptionAtRest={CatalogEncryptionMode=SSE-KMS,SseAwsKmsKeyId=<ksm_key_arn>",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_37#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/data-catalog-encryption-at-rest.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_37#terraform"
"CLI": "aws glue put-data-catalog-encryption-settings --data-catalog-encryption-settings '{\"EncryptionAtRest\":{\"CatalogEncryptionMode\":\"SSE-KMS\"}}'",
"NativeIaC": "```yaml\n# Enable Glue Data Catalog metadata encryption with KMS\nResources:\n <example_resource_name>:\n Type: AWS::Glue::DataCatalogEncryptionSettings\n Properties:\n DataCatalogEncryptionSettings:\n EncryptionAtRest:\n CatalogEncryptionMode: SSE-KMS # Critical: enables KMS encryption for catalog metadata\n```",
"Other": "1. In the AWS Console, go to AWS Glue\n2. Open Data Catalog > Settings\n3. Under Security configuration and encryption, check Metadata encryption\n4. Leave the default AWS managed key selected (or choose a KMS key)\n5. Click Save",
"Terraform": "```hcl\n# Enable Glue Data Catalog metadata encryption with KMS\nresource \"aws_glue_data_catalog_encryption_settings\" \"<example_resource_name>\" {\n data_catalog_encryption_settings {\n encryption_at_rest {\n catalog_encryption_mode = \"SSE-KMS\" # Critical: turns on KMS encryption for catalog metadata\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable Encryption. Use a CMK where possible. It will provide additional management and privacy benefits.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/encrypt-glue-data-catalog.html"
"Text": "Enable metadata encryption with **`SSE-KMS`**, preferably using a **customer-managed KMS key** for control and rotation.\n\nApply **least privilege** to KMS and catalog access, restrict who can change settings, and monitor key usage. Use **defense in depth** by encrypting related analytics assets consistently.",
"Url": "https://hub.prowler.com/check/glue_data_catalogs_metadata_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,35 @@
{
"Provider": "aws",
"CheckID": "glue_data_catalogs_not_publicly_accessible",
"CheckTitle": "Ensure Glue Data Catalogs are not publicly accessible.",
"CheckTitle": "Glue Data Catalog is not publicly accessible via its resource policy",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
"Software and Configuration Checks/AWS Security Best Practices/Network Reachability",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"TTPs/Initial Access",
"Effects/Data Exposure"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:glue:region:account-id:catalog",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsGlueDataCatalog",
"Description": "This control checks whether Glue Data Catalogs are not publicly accessible via resource policies.",
"Risk": "Publicly accessible Glue Data Catalogs can expose sensitive data schema and metadata, leading to potential security risks.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/security_iam_service-with-iam.html?icmpid=docs_console_unmapped#security_iam_service-with-iam-resource-based-policies",
"ResourceType": "Other",
"Description": "**AWS Glue Data Catalog** resource policies are assessed for configurations that expose the catalog to anyone, such as `Principal: *`, broad resource scopes, or permissive conditions.\n\nThe finding highlights catalogs made public through overly permissive resource-based access.",
"Risk": "Public catalog access lets unauthorized actors enumerate schemas, S3 locations, and connection metadata, weakening **confidentiality**. If writes are exposed, attackers can alter databases/tables, corrupt lineage, and disrupt jobs and queries, harming **integrity** and **availability**, and enabling lateral movement to data stores.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/security_iam_service-with-iam.html?icmpid=docs_console_unmapped#security_iam_service-with-iam-resource-based-policies",
"https://docs.aws.amazon.com/glue/latest/dg/cross-account-access.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue delete-resource-policy",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Sign in to the AWS Console and open the Glue service\n2. In the left menu, click Settings\n3. Under Data catalog settings > Permissions, click Edit resource policy\n4. Remove any statement that has Principal set to * (public) or AWS: \"*\"; or delete the entire policy\n5. Click Save",
"Terraform": "```hcl\nresource \"aws_glue_resource_policy\" \"<example_resource_name>\" {\n policy = jsonencode({\n Version = \"2012-10-17\",\n Statement = [\n {\n Effect = \"Allow\",\n Principal = { AWS = \"arn:aws:iam::<ACCOUNT_ID>:root\" } # Critical: restricts to your account, removing any public (*) access\n Action = \"glue:*\",\n Resource = \"arn:aws:glue:<REGION>:<ACCOUNT_ID>:catalog\"\n }\n ]\n })\n}\n```"
},
"Recommendation": {
"Text": "Review Glue Data Catalog policies and ensure they are not publicly accessible. Implement the Principle of Least Privilege.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/security_iam_service-with-iam.html?icmpid=docs_console_unmapped#security_iam_service-with-iam-resource-based-policies"
"Text": "Enforce **least privilege** on catalog resource policies:\n- Avoid `Principal: *` and wildcards\n- Grant only required actions to explicit principals\n- Prefer identity-based access or Lake Formation for sharing\n- Limit scope with precise ARNs/conditions and monitor changes for **defense in depth**",
"Url": "https://hub.prowler.com/check/glue_data_catalogs_not_publicly_accessible"
}
},
"Categories": [
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "glue_database_connections_ssl_enabled",
"CheckTitle": "Check if Glue database connection has SSL connection enabled.",
"CheckTitle": "Glue connection has SSL enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"Description": "Check if Glue database connection has SSL connection enabled.",
"Risk": "Data exfiltration could happen if information is not protected in transit.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/encryption-in-transit.html",
"Description": "**AWS Glue connections** require **TLS/SSL** for JDBC when the `JDBC_ENFORCE_SSL` property is set to `true`.\n\nThis evaluates connection definitions to confirm SSL is enforced for traffic to external data stores.",
"Risk": "Absent TLS enforcement, JDBC traffic-including credentials, queries, and results-can be **intercepted or modified** in transit.\n\nThis enables:\n- Confidentiality loss via sniffing/MITM\n- Integrity tampering of queries/results\n- Credential theft leading to broader database access",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/encryption-in-transit.html",
"https://support.icompaas.com/support/solutions/articles/62000233690-ensure-glue-connections-have-ssl-enabled"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "aws glue update-connection --name <example_resource_name> --connection-input '{\"Name\":\"<example_resource_name>\",\"ConnectionType\":\"JDBC\",\"ConnectionProperties\":{\"JDBC_CONNECTION_URL\":\"<example_jdbc_url>\",\"JDBC_ENFORCE_SSL\":\"true\"}}'",
"NativeIaC": "```yaml\n# CloudFormation: Enable SSL on a Glue JDBC connection\nResources:\n <example_resource_name>:\n Type: AWS::Glue::Connection\n Properties:\n ConnectionInput:\n ConnectionType: JDBC\n ConnectionProperties:\n JDBC_CONNECTION_URL: \"<example_jdbc_url>\"\n JDBC_ENFORCE_SSL: \"true\" # Critical: forces SSL for the JDBC connection\n```",
"Other": "1. Open the AWS Console and go to AWS Glue > Data Catalog > Connections\n2. Select the connection and click Edit\n3. In Connection properties (Advanced properties), add key JDBC_ENFORCE_SSL with value true (or check Require SSL)\n4. Click Save",
"Terraform": "```hcl\n# Terraform: Enable SSL on a Glue JDBC connection\nresource \"aws_glue_connection\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n connection_type = \"JDBC\"\n\n connection_properties = {\n JDBC_CONNECTION_URL = \"<example_jdbc_url>\"\n JDBC_ENFORCE_SSL = \"true\" # Critical: forces SSL for the JDBC connection\n }\n}\n```"
},
"Recommendation": {
"Text": "Configure encryption settings for crawlers, ETL jobs and development endpoints using security configurations in AWS Glue.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/encryption-in-transit.html"
"Text": "Enforce **TLS** on all Glue connections (set `JDBC_ENFORCE_SSL=true`) and require encryption on target databases.\n\nApply **defense in depth**: validate certificates, restrict network exposure, prefer private connectivity, and use **least-privilege** credentials with rotation.",
"Url": "https://hub.prowler.com/check/glue_database_connections_ssl_enabled"
}
},
"Categories": [
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "glue_development_endpoints_cloudwatch_logs_encryption_enabled",
"CheckTitle": "Check if Glue development endpoints have CloudWatch logs encryption enabled.",
"CheckTitle": "Glue development endpoint has CloudWatch Logs encryption enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"Description": "Check if Glue development endpoints have CloudWatch logs encryption enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"Description": "**AWS Glue development endpoints** are assessed for an associated **security configuration** that enables **CloudWatch Logs encryption**. It confirms the endpoint references a configuration and that log encryption is not `DISABLED`.",
"Risk": "Unencrypted Glue logs erode **confidentiality**: credentials, connection strings, and data samples may be readable to unintended principals, enabling **lateral movement**.\nLack of KMS-backed encryption weakens **auditability** and **separation of duties**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/cloud-watch-logs-encryption-enabled.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue create-security-configuration --name cw-encrypted-sec-config --encryption-configuration {'CloudWatchEncryption': [{'CloudWatchEncryptionMode': 'SSE-KMS','KmsKeyArn': <kms_arn>}]}",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/cloud-watch-logs-encryption-enabled.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: Glue Security Configuration with CloudWatch Logs encryption enabled\nResources:\n <example_resource_name>:\n Type: AWS::Glue::SecurityConfiguration\n Properties:\n Name: <example_resource_name>\n EncryptionConfiguration:\n CloudWatchEncryption:\n CloudWatchEncryptionMode: SSE-KMS # Critical: enables CloudWatch Logs encryption\n KmsKeyArn: <kms_key_arn> # Critical: KMS key used for encrypting Glue logs\n```",
"Other": "1. In the AWS Console, go to Glue > Security configurations > Add security configuration\n2. Enter a name and enable CloudWatch Logs encryption\n3. Select a KMS key (or enter its ARN) and click Create\n4. Go to Glue > Dev endpoints\n5. Create a new Dev endpoint (or delete and recreate the existing one) and select the new Security configuration\n6. Create the endpoint to apply the encryption",
"Terraform": "```hcl\n# Glue Security Configuration with CloudWatch Logs encryption enabled\nresource \"aws_glue_security_configuration\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n\n encryption_configuration {\n cloudwatch_encryption {\n cloudwatch_encryption_mode = \"SSE-KMS\" # Critical: enables CloudWatch Logs encryption\n kms_key_arn = \"<kms_key_arn>\" # Critical: KMS key used for encrypting Glue logs\n }\n\n # Required blocks for valid config (kept minimal)\n job_bookmarks_encryption { job_bookmarks_encryption_mode = \"DISABLED\" }\n s3_encryption { s3_encryption_mode = \"DISABLED\" }\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable Encryption in the Security configurations.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html"
"Text": "Attach a **security configuration** to all development endpoints with **CloudWatch Logs encryption** enabled using a tightly scoped **KMS key**.\nApply **least privilege** to key and log access, rotate keys, and standardize configs via IaC to enforce **defense in depth**.",
"Url": "https://hub.prowler.com/check/glue_development_endpoints_cloudwatch_logs_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,33 @@
{
"Provider": "aws",
"CheckID": "glue_development_endpoints_job_bookmark_encryption_enabled",
"CheckTitle": "Check if Glue development endpoints have Job bookmark encryption enabled.",
"CheckTitle": "Glue development endpoint has Job Bookmark encryption enabled",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"Description": "Check if Glue development endpoints have Job bookmark encryption enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"Description": "**AWS Glue development endpoints** are assessed for an attached **security configuration** where **job bookmark encryption** is enabled. Endpoints lacking a security configuration are also identified.",
"Risk": "Unencrypted job bookmarks stored in S3 can be read or altered, exposing dataset paths, partitions, and processing state. This enables data discovery, state tampering, and replay/skip of workloads, impacting **confidentiality**, **integrity**, and **availability** of ETL pipelines.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/job-bookmark-encryption-enabled.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue create-security-configuration --name jb-encrypted-sec-config --encryption-configuration {'JobBookmarksEncryption': [{'JobBookmarksEncryptionMode': 'SSE-KMS','KmsKeyArn': <kms_arn>}]}",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/job-bookmark-encryption-enabled.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: Enable Job Bookmark encryption and attach to the Dev Endpoint\nResources:\n GlueSecurityConfiguration:\n Type: AWS::Glue::SecurityConfiguration\n Properties:\n Name: <example_resource_name>\n EncryptionConfiguration:\n JobBookmarksEncryption:\n JobBookmarksEncryptionMode: CSE-KMS # Critical: enables Job Bookmark encryption\n KmsKeyArn: <example_kms_key_arn> # Critical: KMS key used for Job Bookmark encryption\n\n GlueDevEndpoint:\n Type: AWS::Glue::DevEndpoint\n Properties:\n RoleArn: <example_role_arn>\n SecurityConfiguration: !Ref GlueSecurityConfiguration # Critical: attach the security configuration to the Dev Endpoint\n```",
"Other": "1. In the AWS Console, go to Glue > Security configurations > Add security configuration\n2. Enter a name, then under Advanced settings enable Job bookmark encryption and select a KMS key (or enter its ARN); Save\n3. Go to Glue > Dev endpoints\n4. Create a new Dev endpoint (or recreate the existing one) and set Security configuration to the configuration created in step 2\n5. Create the endpoint to apply the setting",
"Terraform": "```hcl\n# Terraform: Enable Job Bookmark encryption and attach to the Dev Endpoint\nresource \"aws_glue_security_configuration\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n\n encryption_configuration {\n job_bookmarks_encryption {\n job_bookmarks_encryption_mode = \"CSE-KMS\" # Critical: enables Job Bookmark encryption\n kms_key_arn = \"<example_kms_key_arn>\" # Critical: KMS key used for Job Bookmark encryption\n }\n }\n}\n\nresource \"aws_glue_dev_endpoint\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n role_arn = \"<example_role_arn>\"\n security_configuration = aws_glue_security_configuration.<example_resource_name>.name # Critical: attach the security configuration\n}\n```"
},
"Recommendation": {
"Text": "Enable Encryption in the Security configurations.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html"
"Text": "Attach a **security configuration** to each development endpoint and enable **job bookmark encryption** with a managed KMS key. Apply **least privilege** to S3 and KMS, rotate keys, and align logs and data stores with consistent encryption for **defense in depth**. Regularly audit endpoints for missing or outdated configurations.",
"Url": "https://hub.prowler.com/check/glue_development_endpoints_job_bookmark_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "glue_development_endpoints_s3_encryption_enabled",
"CheckTitle": "Check if Glue development endpoints have S3 encryption enabled.",
"CheckTitle": "Glue development endpoint has S3 encryption enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"Description": "Check if Glue development endpoints have S3 encryption enabled.",
"Risk": "Data exfiltration could happen if information is not protected. KMS keys provide additional security level to IAM policies.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/encryption-security-configuration.html",
"Description": "**AWS Glue development endpoints** are evaluated for an attached **security configuration** with **S3 encryption**. Endpoints lacking a security configuration, or with `s3_encryption` set to `DISABLED`, are flagged by this check.",
"Risk": "Unencrypted S3 writes from dev endpoints leave ETL outputs, temp data, and scripts readable at rest. A misconfigured bucket or stolen creds can expose sensitive content, harming **confidentiality** and triggering compliance issues.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/s3-encryption-enabled.html",
"https://docs.aws.amazon.com/glue/latest/dg/encryption-security-configuration.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue create-security-configuration --name s3-encrypted-sec-config --encryption-configuration {'S3Encryption': [{'S3EncryptionMode': 'SSE-KMS','KmsKeyArn': <kms_arn>}]}",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/s3-encryption-enabled.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: Glue Dev Endpoint with S3 encryption via Security Configuration\nResources:\n SecurityConfig:\n Type: AWS::Glue::SecurityConfiguration\n Properties:\n Name: <example_resource_name>\n EncryptionConfiguration:\n S3Encryptions:\n - S3EncryptionMode: SSE-S3 # CRITICAL: enables S3 encryption for the security configuration\n\n DevEndpoint:\n Type: AWS::Glue::DevEndpoint\n Properties:\n EndpointName: <example_resource_name>\n RoleArn: <example_role_arn>\n SecurityConfiguration: !Ref SecurityConfig # CRITICAL: attaches the encrypted security configuration to the dev endpoint\n```",
"Other": "1. In the AWS Console, go to AWS Glue > Security configurations > Create security configuration\n2. Under S3 encryption, select Server-side encryption (SSE-S3) and save\n3. Go to AWS Glue > Development endpoints > Create development endpoint\n4. Fill required fields and set Security configuration to the one created in step 2\n5. Create the endpoint and delete the old endpoint (without encryption) if it exists",
"Terraform": "```hcl\n# Terraform: Glue Dev Endpoint with S3 encryption\nresource \"aws_glue_security_configuration\" \"secure\" {\n name = \"<example_resource_name>\"\n encryption_configuration {\n s3_encryption {\n s3_encryption_mode = \"SSE-S3\" # CRITICAL: enables S3 encryption\n }\n }\n}\n\nresource \"aws_glue_dev_endpoint\" \"dev\" {\n name = \"<example_resource_name>\"\n role_arn = \"<example_role_arn>\"\n\n security_configuration = aws_glue_security_configuration.secure.name # CRITICAL: attaches encrypted security configuration\n}\n```"
},
"Recommendation": {
"Text": "Specify AWS KMS keys to use for input and output from S3 and EBS.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/encryption-security-configuration.html"
"Text": "Attach a **Glue security configuration** to each dev endpoint with **S3 encryption** enabled; prefer `SSE-KMS` with customer-managed keys. Enforce **least privilege** on IAM and KMS key policies, and extend encryption to logs and bookmarks for **defense in depth**.",
"Url": "https://hub.prowler.com/check/glue_development_endpoints_s3_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,35 @@
{
"Provider": "aws",
"CheckID": "glue_etl_jobs_amazon_s3_encryption_enabled",
"CheckTitle": "Check if Glue ETL Jobs have S3 encryption enabled.",
"CheckTitle": "Glue job has S3 encryption enabled",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark",
"Effects/Data Exposure"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"Severity": "medium",
"ResourceType": "AwsGlueJob",
"Description": "Check if Glue ETL Jobs have S3 encryption enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"Description": "**AWS Glue ETL jobs** are validated to use **Amazon S3 at-rest encryption** (`SSE-S3` or `SSE-KMS`) when writing outputs, either through an attached security configuration or via job arguments. Jobs missing a security configuration or with S3 encryption disabled are identified.",
"Risk": "Storing job outputs in S3 without **at-rest encryption** weakens **confidentiality**. Plaintext objects can be exposed via misconfigured bucket policies, compromised credentials, or media reuse, and lack **KMS key controls**, rotation, and audit trails-hindering incident response and compliance.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/s3-encryption-enabled.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue create-security-configuration --name s3-encrypted-sec-config --encryption-configuration {'S3Encryption': [{'S3EncryptionMode': 'SSE-KMS','KmsKeyArn': <kms_arn>}]}",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/s3-encryption-enabled.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: Attach a Security Configuration with S3 encryption to a Glue job\nResources:\n GlueSecurityConfiguration:\n Type: AWS::Glue::SecurityConfiguration\n Properties:\n Name: <example_resource_name>\n EncryptionConfiguration:\n S3Encryptions:\n - S3EncryptionMode: SSE-S3 # CRITICAL: Enables S3 encryption for Glue outputs\n\n GlueJob:\n Type: AWS::Glue::Job\n Properties:\n Name: <example_resource_name>\n Role: <example_role_arn>\n Command:\n Name: glueetl\n ScriptLocation: s3://<example_resource_name>/script.py\n SecurityConfiguration: !Ref GlueSecurityConfiguration # CRITICAL: Applies encrypted security configuration to the job\n```",
"Other": "1. In the AWS Console, go to AWS Glue > Security configurations > Create security configuration\n2. Enable S3 encryption and choose SSE-S3 (or SSE-KMS with your key)\n3. Save the configuration\n4. Go to AWS Glue > Jobs > select your job > Edit\n5. Under Job details, set Security configuration to the encrypted configuration you created\n6. Save the job",
"Terraform": "```hcl\n# Terraform: Attach a Security Configuration with S3 encryption to a Glue job\nresource \"aws_glue_security_configuration\" \"sec\" {\n name = \"<example_resource_name>\"\n\n s3_encryption {\n s3_encryption_mode = \"SSE-S3\" # CRITICAL: Enables S3 encryption for Glue outputs\n }\n}\n\nresource \"aws_glue_job\" \"job\" {\n name = \"<example_resource_name>\"\n role_arn = \"<example_role_arn>\"\n\n command {\n script_location = \"s3://<example_resource_name>/script.py\"\n }\n\n security_configuration = aws_glue_security_configuration.sec.name # CRITICAL: Applies encrypted security configuration to the job\n}\n```"
},
"Recommendation": {
"Text": "Provide the encryption properties that are used by crawlers, jobs and development endpoints.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html"
"Text": "Require **S3 encryption** for all Glue jobs via security configurations, preferring **SSE-KMS**. Apply **least privilege** to KMS keys, restrict key usage and rotate regularly. Enforce defense-in-depth with bucket policies that require encrypted writes, and monitor with key and S3 access logs.",
"Url": "https://hub.prowler.com/check/glue_etl_jobs_amazon_s3_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,35 @@
{
"Provider": "aws",
"CheckID": "glue_etl_jobs_cloudwatch_logs_encryption_enabled",
"CheckTitle": "Check if Glue ETL Jobs have CloudWatch Logs encryption enabled.",
"CheckTitle": "Glue ETL job has CloudWatch Logs encryption enabled",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark",
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsGlueJob",
"Description": "Check if Glue ETL Jobs have CloudWatch Logs encryption enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"Description": "**AWS Glue ETL jobs** are evaluated for a **security configuration** with **CloudWatch Logs encryption** (`SSE-KMS`) enabled. Jobs without a security configuration, or with CloudWatch Logs encryption set to `DISABLED`, are highlighted.",
"Risk": "Unencrypted Glue logs weaken **confidentiality**.\n\nLog entries can expose credentials, PII, connection strings, and schema details. Anyone with log storage access can harvest secrets for **lateral movement** and data exfiltration, widening the blast radius of compromises.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/cloud-watch-logs-encryption-enabled.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue create-security-configuration --name cw-encrypted-sec-config --encryption-configuration {'CloudWatchEncryption': [{'CloudWatchEncryptionMode': 'SSE-KMS','KmsKeyArn': <kms_arn>}]}",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/cloud-watch-logs-encryption-enabled.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: enable CloudWatch Logs encryption and attach to the job\nResources:\n ExampleSecurityConfiguration:\n Type: AWS::Glue::SecurityConfiguration\n Properties:\n Name: <example_resource_name>\n EncryptionConfiguration:\n CloudWatchEncryption: # Critical: enable CloudWatch Logs encryption for Glue\n CloudWatchEncryptionMode: SSE-KMS # Critical: must not be DISABLED\n KmsKeyArn: <example_kms_key_arn> # Critical: KMS key used for encryption\n\n ExampleJob:\n Type: AWS::Glue::Job\n Properties:\n Role: <example_role_arn>\n Command:\n Name: glueetl\n ScriptLocation: s3://<example_script_path>\n SecurityConfiguration: !Ref ExampleSecurityConfiguration # Critical: attach security configuration to the job\n```",
"Other": "1. In the AWS Glue console, go to Security configurations > Add security configuration\n2. Enter a name, enable CloudWatch Logs encryption, select SSE-KMS, and choose/provide the KMS key ARN; Save\n3. Go to Jobs, select the target job, click Edit\n4. Set Security configuration to the one created in step 2\n5. Save changes",
"Terraform": "```hcl\n# Enable CloudWatch Logs encryption and attach to the Glue job\nresource \"aws_glue_security_configuration\" \"example_resource_name\" {\n name = \"<example_resource_name>\"\n\n encryption_configuration {\n cloudwatch_encryption {\n cloudwatch_encryption_mode = \"SSE-KMS\" # Critical: enable CW Logs encryption\n kms_key_arn = \"<example_kms_key_arn>\" # Critical: KMS key for encryption\n }\n }\n}\n\nresource \"aws_glue_job\" \"example_resource_name\" {\n name = \"<example_resource_name>\"\n role_arn = \"<example_role_arn>\"\n\n command {\n name = \"glueetl\"\n script_location = \"s3://<example_script_path>\"\n }\n\n security_configuration = aws_glue_security_configuration.example_resource_name.name # Critical: attach security config to job\n}\n```"
},
"Recommendation": {
"Text": "Enable Encryption in the Security configurations.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html"
"Text": "Enable **at-rest encryption** for Glue logs via a **security configuration** using customer-managed KMS keys. Apply **least privilege** to KMS and CloudWatch Logs, rotate keys, and require all jobs to attach an approved configuration. Embed this baseline in IaC for consistent, **defense-in-depth** coverage.",
"Url": "https://hub.prowler.com/check/glue_etl_jobs_cloudwatch_logs_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,33 @@
{
"Provider": "aws",
"CheckID": "glue_etl_jobs_job_bookmark_encryption_enabled",
"CheckTitle": "Check if Glue ETL Jobs have Job bookmark encryption enabled.",
"CheckTitle": "Glue ETL job has Job bookmark encryption enabled",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsGlueJob",
"Description": "Check if Glue ETL Jobs have Job bookmark encryption enabled.",
"Risk": "If not enabled sensitive information at rest is not protected.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"ResourceType": "Other",
"Description": "**AWS Glue ETL jobs** should link a **security configuration** with **job bookmark encryption** enabled. Bookmark encryption must not be `DISABLED` (e.g., use `CSE-KMS`). Jobs lacking a security configuration are treated as not protecting bookmark metadata.",
"Risk": "Unencrypted **job bookmarks** in S3 expose execution state and data pointers, reducing **confidentiality**. Altered bookmarks can trigger reruns, skips, or reprocessing, harming **integrity**. Missing security configs may also leave logs and temporary objects unencrypted.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/job-bookmark-encryption-enabled.html"
],
"Remediation": {
"Code": {
"CLI": "aws glue create-security-configuration --name jb-encrypted-sec-config --encryption-configuration {'JobBookmarksEncryption': [{'JobBookmarksEncryptionMode': 'SSE-KMS','KmsKeyArn': <kms_arn>}]}",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#cloudformation",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Glue/job-bookmark-encryption-enabled.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_41#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: Enable Glue Job bookmark encryption via Security Configuration\nResources:\n <example_resource_name>:\n Type: AWS::Glue::SecurityConfiguration\n Properties:\n Name: <example_resource_name>\n EncryptionConfiguration:\n JobBookmarksEncryption:\n JobBookmarksEncryptionMode: CSE-KMS # CRITICAL: Enables job bookmark encryption\n KmsKeyArn: <example_kms_key_arn> # CRITICAL: KMS key used to encrypt job bookmarks\n```",
"Other": "1. In the AWS Console, go to AWS Glue > Security configurations > Add security configuration\n2. Enter a name and under Advanced settings enable Job bookmark encryption\n3. Select a KMS key (or paste the key ARN) and click Create\n4. Go to AWS Glue > Jobs, select the job, click Edit\n5. Under Advanced properties, set Security configuration to the one created above\n6. Click Save",
"Terraform": "```hcl\n# Terraform: Enable Glue Job bookmark encryption via Security Configuration\nresource \"aws_glue_security_configuration\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n\n encryption_configuration {\n job_bookmarks_encryption {\n job_bookmarks_encryption_mode = \"CSE-KMS\" # CRITICAL: Enables job bookmark encryption\n kms_key_arn = \"<example_kms_key_arn>\" # CRITICAL: KMS key for bookmarks\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable Encryption in the Security configurations.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html"
"Text": "Attach a **Glue security configuration** to every job and enable **job bookmark encryption** (e.g., `CSE-KMS`). Use **customer-managed KMS keys**, enforce **least privilege** on key usage, and rotate keys. For **defense in depth**, also encrypt **S3 temp data** and **CloudWatch logs** in the same configuration.",
"Url": "https://hub.prowler.com/check/glue_etl_jobs_job_bookmark_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "glue_etl_jobs_logging_enabled",
"CheckTitle": "[DEPRECATED] Check if Glue ETL Jobs have logging enabled.",
"CheckTitle": "Glue ETL job has continuous CloudWatch logging enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:glue:region:account-id:job/job-name",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsGlueJob",
"Description": "[DEPRECATED] Ensure that Glue ETL Jobs have CloudWatch logs enabled.",
"Risk": "Without logging enabled, AWS Glue jobs lack visibility into job activities and failures, making it difficult to detect unauthorized access, troubleshoot issues, and ensure compliance. This may result in untracked security incidents or operational issues that affect data processing.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging.html",
"ResourceType": "Other",
"Description": "**AWS Glue jobs** are assessed for **continuous CloudWatch logging**, confirming that runtime events and outputs are sent to **CloudWatch Logs** via the `--enable-continuous-cloudwatch-log` configuration.",
"Risk": "Missing job logs hide execution details and access patterns, enabling undetected credential abuse, data exfiltration in scripts, or tampering with transforms. This reduces confidentiality and integrity, hinders incident response, and can mask failures that impact availability.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging.html",
"https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging-enable.html",
"https://docs.aws.amazon.com/securityhub/latest/userguide/glue-controls.html#glue-2"
],
"Remediation": {
"Code": {
"CLI": "aws glue update-job --job-name <job-name> --job-update \"Command={DefaultArguments={--enable-continuous-cloudwatch-log=true}}\"",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/glue-controls.html#glue-2",
"Terraform": ""
"CLI": "aws glue update-job --job-name <example_resource_name> --job-update '{\"DefaultArguments\":{\"--enable-continuous-cloudwatch-log\":\"true\"}}'",
"NativeIaC": "```yaml\nResources:\n GlueJob:\n Type: AWS::Glue::Job\n Properties:\n Role: \"<example_resource_id>\"\n Command:\n Name: glueetl\n ScriptLocation: \"s3://<example_resource_name>/script.py\"\n DefaultArguments:\n \"--enable-continuous-cloudwatch-log\": \"true\" # Critical: enables continuous CloudWatch logging to pass the check\n```",
"Other": "1. Open the AWS Glue console and go to Jobs\n2. Select the job and click Edit\n3. Expand Advanced properties\n4. Under Continuous logging, check Enable logs in CloudWatch\n5. Save",
"Terraform": "```hcl\nresource \"aws_glue_job\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n role_arn = \"<example_resource_id>\"\n\n command {\n script_location = \"s3://<example_resource_name>/script.py\"\n }\n\n default_arguments = {\n \"--enable-continuous-cloudwatch-log\" = \"true\" # Critical: enables continuous CloudWatch logging to pass the check\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable logging for AWS Glue jobs to capture and monitor job events. Logging allows for better visibility into job performance, error detection, and security oversight.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging-enable.html"
"Text": "Enable **continuous logging** to **CloudWatch Logs** for all Glue jobs. Centralize logs with retention and KMS encryption, restrict read access, and alert on anomalies and failures. Apply **least privilege** to job roles and use **defense in depth** by correlating logs across services.",
"Url": "https://hub.prowler.com/check/glue_etl_jobs_logging_enabled"
}
},
"Categories": [
@@ -1,26 +1,34 @@
{
"Provider": "aws",
"CheckID": "glue_ml_transform_encrypted_at_rest",
"CheckTitle": "Check if Glue ML Transform Encryption at Rest is Enabled",
"CheckType": [],
"CheckTitle": "Glue ML Transform is encrypted at rest",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)"
],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:glue:region:account-id:mlTransform/transform-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"Description": "This control checks whether an AWS Glue machine learning transform is encrypted at rest. The control fails if the machine learning transform isn't encrypted at rest.",
"Risk": "Data at rest refers to data that's stored in persistent, non-volatile storage for any duration. Encrypting data at rest helps you protect its confidentiality, which reduces the risk that an unauthorized user can access it.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/encryption-at-rest.html",
"Description": "**AWS Glue ML transforms** are evaluated for **encryption at rest** of transform user data using **KMS keys**. The finding highlights transforms where encryption is not configured.",
"Risk": "Without encryption, **confidentiality** is weakened: transform artifacts, mappings, and sample datasets may be readable via storage access, backups, or cross-account exposure. This can lead to data disclosure and aid **lateral movement** by revealing schemas and data relationships.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/glue/latest/dg/encryption-at-rest.html",
"https://docs.aws.amazon.com/securityhub/latest/userguide/glue-controls.html#glue-3"
],
"Remediation": {
"Code": {
"CLI": "aws glue update-ml-transform --transform-id <transform-id> --encryption-at-rest {\"Enabled\":true,\"KmsKey\":\"<kms-key-arn>\"}",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/glue-controls.html#glue-3",
"Terraform": ""
"CLI": "aws glue update-ml-transform --transform-id <transform-id> --transform-encryption '{\"MlUserDataEncryption\":{\"MlUserDataEncryptionMode\":\"SSE-KMS\",\"KmsKeyId\":\"<kms-key-arn>\"}}'",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::Glue::MLTransform\n Properties:\n Role: <example_resource_id>\n InputRecordTables:\n - DatabaseName: <example_resource_name>\n TableName: <example_resource_name>\n TransformParameters:\n TransformType: FIND_MATCHES\n FindMatchesParameters:\n PrimaryKeyColumnName: <example_resource_name>\n TransformEncryption:\n MlUserDataEncryption:\n MlUserDataEncryptionMode: SSE-KMS # Critical: enables ML user data encryption at rest\n KmsKeyId: <kms-key-arn> # Critical: KMS key used for encryption\n```",
"Other": "1. In the AWS Management Console, open AWS Glue\n2. Go to Machine learning > Transforms and select the target transform\n3. Click Edit\n4. Under Encryption, enable ML user data encryption\n5. Choose an AWS KMS key\n6. Save changes",
"Terraform": "```hcl\nresource \"aws_glue_ml_transform\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n role_arn = \"<example_resource_id>\"\n\n input_record_tables {\n database_name = \"<example_resource_name>\"\n table_name = \"<example_resource_name>\"\n }\n\n parameters {\n transform_type = \"FIND_MATCHES\"\n find_matches_parameters {\n primary_key_column_name = \"<example_resource_name>\"\n }\n }\n\n transform_encryption {\n ml_user_data_encryption {\n ml_user_data_encryption_mode = \"SSE-KMS\" # Critical: enables encryption at rest\n kms_key_id = \"<kms-key-arn>\" # Critical: KMS key used for encryption\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable encryption at rest for Glue ML Transforms using AWS KMS keys.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/encryption-at-rest.html"
"Text": "Enable **KMS-backed encryption at rest** for all ML transforms and prefer **customer-managed keys**.\n- Apply **least privilege** key policies and rotate keys\n- Enforce **defense in depth** with network and IAM controls\n- Monitor key usage and transform access with audit logs",
"Url": "https://hub.prowler.com/check/glue_ml_transform_encrypted_at_rest"
}
},
"Categories": [
@@ -26,7 +26,9 @@
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege"
}
},
"Categories": [],
"Categories": [
"privilege-escalation"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -27,7 +27,9 @@
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege"
}
},
"Categories": [],
"Categories": [
"privilege-escalation"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "CAF Security Epic: IAM"
@@ -26,7 +26,7 @@
}
},
"Categories": [
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -24,7 +24,7 @@
}
},
"Categories": [
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -24,7 +24,7 @@
}
},
"Categories": [
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -24,7 +24,7 @@
}
},
"Categories": [
"trustboundaries"
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,29 +1,39 @@
{
"Provider": "aws",
"CheckID": "inspector2_active_findings_exist",
"CheckTitle": "Check if Inspector2 active findings exist",
"CheckTitle": "Inspector2 is enabled with no active findings",
"CheckAliases": [
"inspector2_findings_exist"
],
"CheckType": [],
"CheckType": [
"Software and Configuration Checks/Vulnerabilities/CVE",
"Software and Configuration Checks/Patch Management",
"Software and Configuration Checks/AWS Security Best Practices",
"Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "inspector2",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:inspector2:region:account-id/detector-id",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"Description": "This check determines if there are any active findings in your AWS account that have been detected by AWS Inspector2. Inspector2 is an automated security assessment service that helps improve the security and compliance of applications deployed on AWS.",
"Risk": "Without using AWS Inspector, you may not be aware of all the security vulnerabilities in your AWS resources, which could lead to unauthorized access, data breaches, or other security incidents.",
"RelatedUrl": "https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
"Description": "**Amazon Inspector2** active findings are assessed across eligible resources when the service is `ENABLED`.\n\nIndicates whether any findings remain in the **Active** state versus none.",
"Risk": "**Unremediated Inspector2 findings** mean known vulnerabilities or exposures persist on workloads.\n\nThis enables:\n- Unauthorized access and data exfiltration (C)\n- Code tampering and privilege escalation (I)\n- Service disruption via exploitation or malware (A)",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector/amazon-inspector-findings.html",
"https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
"https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector/amazon-inspector-findings.html",
"Terraform": ""
"CLI": "aws inspector2 create-filter --name <example_resource_name> --action SUPPRESS --filter-criteria '{\"findingStatus\":[{\"comparison\":\"EQUALS\",\"value\":\"ACTIVE\"}]}'",
"NativeIaC": "```yaml\n# CloudFormation: Suppress all ACTIVE Inspector findings\nResources:\n <example_resource_name>:\n Type: AWS::InspectorV2::Filter\n Properties:\n Name: <example_resource_name>\n Action: SUPPRESS # critical: converts matching findings to Suppressed, not Active\n FilterCriteria:\n FindingStatus:\n - Comparison: EQUALS\n Value: ACTIVE # critical: targets all active findings\n```",
"Other": "1. In the AWS Console, go to Amazon Inspector\n2. Open Suppression rules (or Filters) and click Create suppression rule\n3. Set condition: Finding status = Active\n4. Set action to Suppress and click Create\n5. Verify the Active findings count is 0 on the dashboard",
"Terraform": "```hcl\n# Terraform: Suppress all ACTIVE Inspector findings\nresource \"aws_inspector2_filter\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n action = \"SUPPRESS\" # critical: converts matching findings to Suppressed, not Active\n\n filter_criteria {\n finding_status {\n comparison = \"EQUALS\"\n value = \"ACTIVE\" # critical: targets all active findings\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Review the active findings from Inspector2",
"Url": "https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html"
"Text": "Prioritize and remediate **Active findings** quickly: patch hosts and runtimes, update/rebuild images, fix vulnerable code, and close unintended exposure.\n\nApply **least privilege**, use **defense in depth**, and avoid broad suppressions. Integrate findings into CI/CD and vulnerability management for continuous prevention.",
"Url": "https://hub.prowler.com/check/inspector2_active_findings_exist"
}
},
"Categories": [],
@@ -1,31 +1,37 @@
{
"Provider": "aws",
"CheckID": "inspector2_is_enabled",
"CheckTitle": "Check if Inspector2 is enabled for Amazon EC2 instances, ECR container images and Lambda functions.",
"CheckTitle": "Inspector2 is enabled for Amazon EC2 instances, ECR container images, Lambda functions, and Lambda code",
"CheckAliases": [
"inspector2_findings_exist"
],
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "inspector2",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:inspector2:region:account-id/detector-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsAccount",
"Description": "Ensure that the new version of Amazon Inspector is enabled in order to help you improve the security and compliance of your AWS cloud environment. Amazon Inspector 2 is a vulnerability management solution that continually scans scans your Amazon EC2 instances, ECR container images, and Lambda functions to identify software vulnerabilities and instances of unintended network exposure.",
"Risk": "Without using AWS Inspector, you may not be aware of all the security vulnerabilities in your AWS resources, which could lead to unauthorized access, data breaches, or other security incidents.",
"RelatedUrl": "https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
"ResourceType": "Other",
"Description": "**Amazon Inspector 2** activation and coverage across regions, verifying that scanning is active for **EC2**, **ECR**, **Lambda functions**, and **Lambda code** where applicable.\n\nIt flags missing account activation or gaps in any scan type.",
"Risk": "Absent or partial coverage leaves **unpatched vulnerabilities**, risky **code dependencies**, and **unintended network exposure** undetected.\n\nAttackers can exploit known CVEs for **remote code execution**, **lateral movement**, and **data exfiltration**, degrading **confidentiality**, **integrity**, and **availability**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector2/enable-amazon-inspector2.html",
"https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
"https://docs.aws.amazon.com/inspector/latest/user/getting_started_tutorial.html"
],
"Remediation": {
"Code": {
"CLI": "aws inspector2 enable --resource-types 'EC2' 'ECR' 'LAMBDA' 'LAMBDA_CODE'",
"CLI": "aws inspector2 enable --resource-types EC2 ECR LAMBDA LAMBDA_CODE",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector2/enable-amazon-inspector2.html",
"Terraform": ""
"Other": "1. Sign in to the AWS Console and open Amazon Inspector (v2)\n2. If not yet activated: click Get started > Activate Amazon Inspector\n3. If already activated: go to Settings > Scans and ensure EC2, ECR, Lambda functions, and Lambda code are all enabled, then Save",
"Terraform": "```hcl\nresource \"aws_inspector2_enabler\" \"<example_resource_name>\" {\n resource_types = [\"EC2\", \"ECR\", \"LAMBDA\", \"LAMBDA_CODE\"] # Enables Inspector2 scans for all required resource types\n}\n```"
},
"Recommendation": {
"Text": "Enable Amazon Inspector 2 for your AWS account.",
"Url": "https://docs.aws.amazon.com/inspector/latest/user/getting_started_tutorial.html"
"Text": "Enable **Amazon Inspector 2** across all regions and activate scans for **EC2**, **ECR**, **Lambda**, and **Lambda code**.\n\nApply **defense in depth**: auto-enable coverage for new workloads, integrate findings with patching and CI/CD gates, enforce remediation SLAs, and grant only **least privilege** to process and act on findings.",
"Url": "https://hub.prowler.com/check/inspector2_is_enabled"
}
},
"Categories": [],
@@ -1,31 +1,42 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_encryption_at_rest_uses_cmk",
"CheckTitle": "Ensure Kafka Cluster Encryption at Rest Uses Customer Managed Keys (CMK)",
"CheckTitle": "Kafka cluster has encryption at rest enabled with a customer managed key (CMK) or is serverless",
"CheckType": [
"Infrastructure Security"
"Software and Configuration Checks/AWS Security Best Practices/Data Encryption",
"Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Industry and Regulatory Standards/NIST 800-53 Controls (USA)",
"Industry and Regulatory Standards/PCI-DSS",
"Effects/Data Exposure"
],
"ServiceName": "kafka",
"SubServiceName": "Kafka Cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsMskCluster",
"Description": "Kafka Cluster data stored at rest should be encrypted using Customer Managed Keys (CMK) for enhanced security and control over the encryption process.",
"Risk": "Using default AWS-managed encryption keys might not meet certain compliance or regulatory requirements. With CMKs, you have more control over the encryption process and can rotate keys, define access policies, and enable key auditing.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html",
"Description": "Amazon MSK clusters are inspected for **encryption at rest** using a **customer-managed KMS key** for data volumes. Serverless clusters are inherently encrypted. Provisioned clusters are recognized only when the configured `DataVolumeKMSKeyId` corresponds to a customer-managed key.",
"Risk": "Relying on service-managed keys weakens **confidentiality** and **accountability**: you can't enforce granular key policies, separation of duties, or independent rotation. This limits incident response (e.g., disabling the key for crypto-shredding) and reduces auditability, increasing impact of credential misuse or broker compromise.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/msk-encryption-at-rest-with-cmk.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-working-with-encryption.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/msk-encryption-at-rest-with-cmk.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_32/#terraform"
"NativeIaC": "```yaml\n# CloudFormation: MSK cluster using a customer managed KMS key for encryption at rest\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: <KAFKA_VERSION>\n NumberOfBrokerNodes: 2\n BrokerNodeGroupInfo:\n InstanceType: kafka.m5.large\n ClientSubnets:\n - <example_subnet_id_a>\n - <example_subnet_id_b>\n SecurityGroups:\n - <example_security_group_id>\n EncryptionInfo:\n EncryptionAtRest:\n DataVolumeKMSKeyId: <example_kms_key_arn> # Critical: use a customer managed KMS key ARN to enable CMK encryption at rest\n```",
"Other": "1. In the AWS Console, go to Amazon MSK > Clusters\n2. Click Create cluster\n3. Choose Provisioned (or choose Serverless to pass by default)\n4. In Encryption settings, for At-rest encryption, select Customer managed key and choose your CMK (not alias/aws/kafka)\n5. Create the cluster, migrate clients to it, then delete the old cluster that used the AWS managed key",
"Terraform": "```hcl\n# MSK cluster using a customer managed KMS key for encryption at rest\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"<KAFKA_VERSION>\"\n number_of_broker_nodes = 2\n\n broker_node_group_info {\n instance_type = \"kafka.m5.large\"\n client_subnets = [\"<example_subnet_id_a>\", \"<example_subnet_id_b>\"]\n security_groups = [\"<example_security_group_id>\"]\n }\n\n encryption_info {\n encryption_at_rest_kms_key_arn = \"<example_kms_key_arn>\" # Critical: customer managed KMS key to pass the check\n }\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to use Customer Managed Keys (CMK) for Kafka Cluster encryption at rest to maintain control and flexibility over the encryption process.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-working-with-encryption.html"
"Text": "Use a **customer-managed KMS key** for MSK at-rest encryption. Apply **least privilege** in key policies and grants, enable **key rotation**, and log key use for auditing. Enforce **separation of duties** between MSK admins and KMS key custodians, and regularly review access, aliases, and pending-deletion states.",
"Url": "https://hub.prowler.com/check/kafka_cluster_encryption_at_rest_uses_cmk"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,29 +1,39 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_enhanced_monitoring_enabled",
"CheckTitle": "Ensure Enhanced Monitoring is Enabled for MSK (Kafka) Brokers",
"CheckType": [],
"CheckTitle": "Amazon MSK cluster has enhanced monitoring enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kafka",
"SubServiceName": "cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsMskCluster",
"Description": "Enhanced monitoring provides additional visibility into the performance and behavior of MSK (Kafka) brokers. By enabling enhanced monitoring, you can gain insights into potential issues and optimize the performance of your Kafka clusters.",
"Risk": "Without enhanced monitoring, you may have limited visibility into the performance and health of your MSK brokers, which could lead to undetected issues and potential performance degradation.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/monitoring.html",
"Description": "**Amazon MSK clusters** are assessed for **enhanced monitoring** levels beyond `DEFAULT` (e.g., `PER_BROKER`, `PER_TOPIC_PER_BROKER`, `PER_TOPIC_PER_PARTITION`).\n\n*Serverless clusters* include enhanced monitoring by design; provisioned clusters are evaluated by their configured monitoring level.",
"Risk": "Insufficient metrics limit visibility into **broker health**, **replication state**, and **consumer lag**, delaying response to incidents.\n\nThis increases risk of **availability loss** (saturation, throttling) and can mask **integrity issues** such as under-replicated partitions, raising data-loss impact during failures.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/metrics-details.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/enable-enhanced-monitoring-for-apache-kafka-brokers.html#",
"https://docs.aws.amazon.com/msk/latest/developerguide/monitoring.html"
],
"Remediation": {
"Code": {
"CLI": "aws kafka update-monitoring --region region_cluster --cluster-arn arn_cluster --current-version version_cluster --enhanced-monitoring PER_BROKER",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/enable-enhanced-monitoring-for-apache-kafka-brokers.html#",
"Terraform": ""
"CLI": "aws kafka update-monitoring --cluster-arn <CLUSTER_ARN> --current-version <CURRENT_VERSION> --enhanced-monitoring PER_BROKER",
"NativeIaC": "```yaml\n# CloudFormation: Enable enhanced monitoring on an MSK cluster\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: <example_kafka_version>\n NumberOfBrokerNodes: 2\n BrokerNodeGroupInfo:\n ClientSubnets:\n - <example_subnet_id_1>\n - <example_subnet_id_2>\n InstanceType: kafka.t3.small\n EnhancedMonitoring: PER_BROKER # Critical: sets enhanced monitoring above DEFAULT to pass the check\n```",
"Other": "1. Open the AWS Console and go to Amazon MSK\n2. Select your provisioned cluster\n3. Click Edit\n4. Under Monitoring, set Enhanced monitoring to PER_BROKER (or higher)\n5. Save changes and wait for the update to complete",
"Terraform": "```hcl\n# Terraform: Enable enhanced monitoring on an MSK cluster\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"<example_kafka_version>\"\n number_of_broker_nodes = 2\n\n broker_node_group_info {\n instance_type = \"kafka.t3.small\"\n client_subnets = [\"<example_subnet_id_1>\", \"<example_subnet_id_2>\"]\n }\n\n enhanced_monitoring = \"PER_BROKER\" # Critical: sets monitoring above DEFAULT to pass the check\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to enable enhanced monitoring for MSK (Kafka) brokers to gain deeper insights into the performance and behavior of your clusters.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/metrics-details.html"
"Text": "Select an enhanced level (e.g., `PER_BROKER` or finer) and establish **observability**: prioritize telemetry for broker resources, replication health, and consumer lag. Configure alerts and dashboards aligned to SLOs to enable proactive scaling and rapid incident containment. *Balance granularity with cost*.",
"Url": "https://hub.prowler.com/check/kafka_cluster_enhanced_monitoring_enabled"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,31 +1,39 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_in_transit_encryption_enabled",
"CheckTitle": "Ensure Kafka Cluster Encryption in Transit is Enabled",
"CheckTitle": "Kafka cluster has encryption in transit enabled",
"CheckType": [
"Infrastructure Security"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kafka",
"SubServiceName": "cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"Severity": "medium",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsMskCluster",
"Description": "Kafka clusters should have encryption in transit enabled to protect data as it travels across the network. This ensures that data is encrypted when transmitted between clients and brokers, preventing unauthorized access or data breaches.",
"Risk": "If encryption in transit is not enabled, data transmitted over the network could be vulnerable to eavesdropping or man-in-the-middle attacks.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html",
"Description": "**Amazon MSK clusters** are evaluated for **encryption in transit** on both paths: **clientbroker** set to `TLS` only and **inter-broker** encryption enabled. *Serverless clusters provide this by default*.\n\nThe finding highlights clusters where client-broker traffic isn't `TLS`-only or inter-broker encryption is turned off.",
"Risk": "Unencrypted or mixed (`TLS_PLAINTEXT`/`PLAINTEXT`) traffic enables interception of records, credentials, and metadata, supporting **MITM**, replay, and message tampering.\n\nPlaintext inter-broker links expose replication data within the VPC, enabling **lateral movement** and topic poisoning, degrading data **confidentiality** and **integrity**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-working-with-encryption.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/encryption-in-transit-for-msk.html"
],
"Remediation": {
"Code": {
"CLI": "aws kafka create-cluster --cluster-name <CLUSTER_NAME> --broker-node-group-info <NODE_JSON> --encryption-info <INFO_JSON> --kafka-version <VERSION> --number-of-broker-nodes <NUMBER>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/encryption-in-transit-for-msk.html",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_32/#terraform"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: MSK cluster with encryption in transit enforced\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: <VERSION>\n NumberOfBrokerNodes: 3\n BrokerNodeGroupInfo:\n ClientSubnets:\n - <example_resource_id>\n - <example_resource_id>\n InstanceType: kafka.m5.large\n EncryptionInfo:\n EncryptionInTransit:\n ClientBroker: TLS # Critical: forces client-to-broker TLS only\n InCluster: true # Critical: enables inter-broker encryption\n```",
"Other": "1. In the AWS Console, go to Amazon MSK > Clusters and select your cluster\n2. Click Edit (Security)\n3. Under Encryption in transit, set Client-broker to TLS only\n4. Save changes\n5. Verify Inter-broker (in-cluster) encryption is enabled; if it is disabled (immutable), create a new cluster with:\n - Encryption in transit: Client-broker = TLS only, Inter-broker encryption = Enabled\n - Migrate clients to the new cluster, then decommission the old one",
"Terraform": "```hcl\n# Terraform: MSK cluster with encryption in transit enforced\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"<VERSION>\"\n number_of_broker_nodes = 3\n\n broker_node_group_info {\n instance_type = \"kafka.m5.large\"\n client_subnets = [\n \"subnet-<example_resource_id>\",\n \"subnet-<example_resource_id>\",\n ]\n }\n\n encryption_info {\n encryption_in_transit {\n client_broker = \"TLS\" # Critical: forces client-to-broker TLS only\n in_cluster = true # Critical: enables inter-broker encryption\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to enable encryption in transit for Kafka clusters to protect data confidentiality and integrity.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-working-with-encryption.html"
"Text": "Enforce end-to-end transport protection:\n- Require `client_broker=TLS` for all clients\n- Enable `in_cluster=true` for broker-to-broker links\n\nApply **defense in depth**: restrict network paths, prefer private connectivity, and use strong client authentication with **least privilege** authorization to limit blast radius.",
"Url": "https://hub.prowler.com/check/kafka_cluster_in_transit_encryption_enabled"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,26 +1,35 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_is_public",
"CheckTitle": "Kafka Cluster Exposed to the Public",
"CheckType": [],
"CheckTitle": "Kafka cluster is not publicly accessible",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Network Reachability",
"TTPs/Initial Access",
"Effects/Data Exposure"
],
"ServiceName": "kafka",
"SubServiceName": "cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"Severity": "high",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "AwsMskCluster",
"Description": "The Kafka cluster is publicly accessible, which can expose sensitive data and increase the attack surface.",
"Risk": "Exposing the Kafka cluster to the public can lead to unauthorized access, data breaches, and potential security threats.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/client-access.html",
"Description": "**Amazon MSK clusters** with broker endpoints **exposed to the public Internet**.\n\nServerless clusters are private by default; provisioned clusters are evaluated for their `public access` configuration.",
"Risk": "Public brokers erode **CIA**:\n- **Confidentiality**: unauthorized consumers can read topics\n- **Integrity**: rogue producers inject or alter events\n- **Availability**: floods or scans strain brokers\n\nThis enables metadata enumeration, data exfiltration, stream poisoning, and costly egress.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/public-access.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/public-access-msk-cluster.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/client-access.html"
],
"Remediation": {
"Code": {
"CLI": "aws kafka update-connectivity --cluster-arn cluster_arn --current-version kafka_version --connectivity-info '{\"PublicAccess\": {\"Type\": \"DISABLED\"}}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/public-access-msk-cluster.html",
"Terraform": ""
"CLI": "aws kafka update-connectivity --cluster-arn <CLUSTER_ARN> --current-version <CURRENT_CLUSTER_VERSION> --connectivity-info '{\"PublicAccess\":{\"Type\":\"DISABLED\"}}'",
"NativeIaC": "```yaml\n# CloudFormation: ensure MSK cluster is not publicly accessible\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: \"2.8.1\"\n NumberOfBrokerNodes: 2\n BrokerNodeGroupInfo:\n ClientSubnets:\n - <example_subnet_id_1>\n - <example_subnet_id_2>\n InstanceType: kafka.t3.small\n ConnectivityInfo:\n PublicAccess:\n Type: DISABLED # Critical: disables public access to brokers\n```",
"Other": "1. Open the Amazon MSK console\n2. Select your cluster and go to the Properties tab\n3. In Network settings, click Edit public access\n4. Set Public access to Disabled (Off)\n5. Click Save changes",
"Terraform": "```hcl\n# Terraform: ensure MSK cluster is not publicly accessible\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"2.8.1\"\n number_of_broker_nodes = 2\n\n broker_node_group_info {\n client_subnets = [\n \"<example_subnet_id_1>\",\n \"<example_subnet_id_2>\",\n ]\n instance_type = \"kafka.t3.small\"\n\n connectivity_info {\n public_access {\n type = \"DISABLED\" # Critical: disables public access to brokers\n }\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to restrict access to the Kafka cluster to only authorized entities. Enable encryption for data in transit and at rest to protect sensitive information.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/public-access.html"
"Text": "Keep brokers private within the VPC by disabling public access and limiting exposure to trusted networks.\n\nEnforce strong auth (SASL/IAM, SASL/SCRAM, or mTLS), require TLS, and apply Kafka ACLs. Provide access via VPN, bastion, or private networking (peering/Transit Gateway). Apply **least privilege** and monitor broker connections.",
"Url": "https://hub.prowler.com/check/kafka_cluster_is_public"
}
},
"Categories": [
@@ -1,29 +1,40 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_mutual_tls_authentication_enabled",
"CheckTitle": "Ensure Mutual TLS Authentication is Enabled for Kafka Cluster",
"CheckType": [],
"CheckTitle": "Kafka cluster has TLS authentication enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"TTPs/Initial Access"
],
"ServiceName": "kafka",
"SubServiceName": "cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"Severity": "medium",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsMskCluster",
"Description": "Mutual TLS Authentication ensures that both the client and the server are authenticated, providing an additional layer of security for communication within the Kafka cluster.",
"Risk": "Without Mutual TLS Authentication, the cluster is vulnerable to man-in-the-middle attacks, and unauthorized clients may be able to access the cluster.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-authentication.html",
"Description": "Amazon MSK clusters enforce **client authentication** on client-to-broker connections. Serverless clusters use TLS-based authentication by default; provisioned clusters must have **mutual TLS (mTLS)** explicitly enabled.",
"Risk": "Without **mTLS**, adversaries can impersonate clients or intercept sessions, compromising **confidentiality** and **integrity**. Unauthorized producers/consumers can read or alter topics, poison data streams, and flood brokers, degrading **availability** and impacting downstream systems.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/enable-mutual-tls-authentication-for-kafka-clients.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-update-security.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-authentication.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/enable-mutual-tls-authentication-for-kafka-clients.html",
"Terraform": ""
"CLI": "aws kafka update-security --cluster-arn <CLUSTER_ARN> --current-version <CURRENT_VERSION> --client-authentication 'Tls={CertificateAuthorityArnList=[\"<ACM_PCA_ARN>\"]}' --encryption-info 'EncryptionInTransit={ClientBroker=TLS}'",
"NativeIaC": "```yaml\n# CloudFormation: Enable mTLS for an MSK cluster\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: <example_kafka_version>\n NumberOfBrokerNodes: 2\n BrokerNodeGroupInfo:\n InstanceType: kafka.m5.large\n ClientSubnets:\n - <subnet_id_1>\n - <subnet_id_2>\n ClientAuthentication:\n Tls:\n CertificateAuthorityArnList:\n - <acm_pca_arn> # CRITICAL: Enables mutual TLS using this Private CA\n EncryptionInfo:\n EncryptionInTransit:\n ClientBroker: TLS # CRITICAL: Required when enabling mTLS\n```",
"Other": "1. In the AWS Console, go to Amazon MSK > Clusters and select the provisioned cluster (state must be ACTIVE)\n2. Choose Actions > Update security (or Security > Edit)\n3. Under Client authentication, enable TLS and add your AWS Private CA ARN(s)\n4. Under Encryption in transit, set Client-broker to TLS\n5. Save/Update and wait for the update to complete",
"Terraform": "```hcl\n# Terraform: Enable mTLS for an MSK cluster\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"<example_kafka_version>\"\n number_of_broker_nodes = 2\n\n broker_node_group_info {\n instance_type = \"kafka.m5.large\"\n client_subnets = [\"<subnet_id_1>\", \"<subnet_id_2>\"]\n }\n\n client_authentication {\n tls {\n certificate_authority_arns = [\"<acm_pca_arn>\"] # CRITICAL: Enables mutual TLS with this Private CA\n }\n }\n\n encryption_info {\n encryption_in_transit {\n client_broker = \"TLS\" # CRITICAL: Required when enabling mTLS\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to enable Mutual TLS Authentication for your Kafka cluster to ensure secure communication between clients and brokers.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-update-security.html"
"Text": "Enable **mutual TLS** for client-broker traffic and disable `PLAINTEXT` listeners. Issue short-lived client certificates from a managed CA with rotation. Apply **least privilege** using Kafka ACLs, restrict network access to trusted sources, and monitor authentication events as part of **defense in depth**.",
"Url": "https://hub.prowler.com/check/kafka_cluster_mutual_tls_authentication_enabled"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,29 +1,41 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_unrestricted_access_disabled",
"CheckTitle": "Ensure Kafka Cluster has unrestricted access disabled",
"CheckType": [],
"CheckTitle": "Kafka cluster requires authentication",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"TTPs/Initial Access/Unauthorized Access",
"Effects/Data Exposure"
],
"ServiceName": "kafka",
"SubServiceName": "cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"Severity": "high",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "AwsMskCluster",
"Description": "Kafka Clusters should not have unrestricted access enabled. Unrestricted access allows anyone to access the Kafka Cluster without any authentication. It is recommended to disable unrestricted access to prevent unauthorized access to the Kafka Cluster.",
"Risk": "Unrestricted access to Kafka Clusters can lead to unauthorized access to the cluster and its data. It is recommended to restrict access to Kafka Clusters to only authorized entities.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-configure-security.html",
"Description": "Amazon MSK clusters are evaluated for **unauthenticated client access**. Serverless clusters inherently require authentication; provisioned clusters are checked for configurations that allow **unrestricted connections** rather than authenticated clients.",
"Risk": "Allowing **unauthenticated access** lets anyone connect and:\n- Read sensitive topics (confidentiality)\n- Publish or alter data (integrity)\n- Overload brokers and consumers (availability)\n\nThis enables message exfiltration, stream poisoning, and abuse of trusted data pipelines.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-configure-security.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/security.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/unrestricted-access-to-brokers.html"
],
"Remediation": {
"Code": {
"CLI": "aws kafka update-security --region region_name --cluster-arn cluster_arn --current-version kafka_version_of_cluster --client-authentication 'Unauthenticated={Enabled=false}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/unrestricted-access-to-brokers.html",
"Terraform": ""
"CLI": "aws kafka update-security --cluster-arn <example_resource_arn> --current-version <example_current_version> --client-authentication 'Unauthenticated={Enabled=false}'",
"NativeIaC": "```yaml\n# CloudFormation: Disable unauthenticated client access for MSK\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: <example_kafka_version>\n NumberOfBrokerNodes: 2\n BrokerNodeGroupInfo:\n InstanceType: <example_instance_type>\n ClientSubnets:\n - <subnet_id_1>\n - <subnet_id_2>\n StorageInfo:\n EbsStorageInfo:\n VolumeSize: 1000\n ClientAuthentication:\n Unauthenticated:\n Enabled: false # CRITICAL: Disables unauthenticated client access\n```",
"Other": "1. Open the AWS Console and go to Amazon MSK\n2. Select your cluster and open the Security tab\n3. Click Edit under Client authentication\n4. Turn off/clear Unauthenticated access\n5. Save changes to apply the update",
"Terraform": "```hcl\n# Terraform: Disable unauthenticated client access for MSK\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"<example_kafka_version>\"\n number_of_broker_nodes = 2\n\n broker_node_group_info {\n instance_type = \"<example_instance_type>\"\n client_subnets = [\"<subnet_id_1>\", \"<subnet_id_2>\"]\n ebs_volume_size = 1000\n }\n\n client_authentication {\n unauthenticated = false # CRITICAL: Disables unauthenticated client access\n }\n}\n```"
},
"Recommendation": {
"Text": "It is recommended to restrict access to Kafka Clusters to only authorized entities. Ensure that the Kafka Cluster's security settings are properly configured to prevent unauthorized access.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/security.html"
"Text": "Disable **unauthenticated access** and require **strong client authentication** (mTLS or IAM/SASL).\n- Enforce **least privilege** with scoped ACLs\n- Restrict network paths via private connectivity and tight security groups\n- Encrypt in transit, monitor access, and rotate credentials regularly",
"Url": "https://hub.prowler.com/check/kafka_cluster_unrestricted_access_disabled"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,31 +1,40 @@
{
"Provider": "aws",
"CheckID": "kafka_cluster_uses_latest_version",
"CheckTitle": "MSK cluster should use the latest version.",
"CheckTitle": "MSK cluster uses the latest Kafka version or is serverless with AWS-managed version",
"CheckType": [
"Infrastructure Security"
"Software and Configuration Checks/Patch Management",
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kafka",
"SubServiceName": "cluster",
"ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsMskCluster",
"Description": "Ensure that your Amazon Managed Streaming for Apache Kafka (MSK) cluster is using the latest version to benefit from the latest security features, bug fixes, and performance improvements.",
"Risk": "Running an outdated version of Amazon MSK may expose your cluster to security vulnerabilities, bugs, and performance issues.",
"RelatedUrl": "https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-databases.html",
"Description": "**Amazon MSK clusters** are evaluated for use of the latest supported **Apache Kafka version**. Provisioned clusters are compared to the most recent release, while **serverless clusters** are treated as automatically managed for versioning.",
"Risk": "Outdated Kafka enables exploitation of known flaws and weak cryptography, risking data exposure or tampering (**confidentiality/integrity**). Missing fixes increase broker crashes and partition instability (**availability**). After end of support, silent auto-upgrades can trigger unexpected behavior and compatibility issues.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/version-support.html#version-upgrades",
"https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-databases.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/enable-apache-kafka-latest-security-features.html"
],
"Remediation": {
"Code": {
"CLI": "aws kafka update-cluster-configuration --cluster-arn <arn_cluster> --current-version <current_version> --target-version <latest_version>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MSK/enable-apache-kafka-latest-security-features.html",
"Terraform": ""
"CLI": "aws kafka update-cluster-kafka-version --cluster-arn <example_resource_id> --current-version <current_version> --target-kafka-version <latest_version>",
"NativeIaC": "```yaml\n# CloudFormation: Upgrade MSK cluster to latest Kafka version\nResources:\n <example_resource_name>:\n Type: AWS::MSK::Cluster\n Properties:\n ClusterName: <example_resource_name>\n KafkaVersion: <latest_version> # CRITICAL: set to the latest Kafka version to pass the check\n NumberOfBrokerNodes: 2\n BrokerNodeGroupInfo:\n InstanceType: kafka.m5.large\n ClientSubnets:\n - <example_resource_id>\n - <example_resource_id>\n```",
"Other": "1. Open the AWS Management Console and go to Amazon MSK\n2. Select your cluster and choose Actions > Update cluster\n3. In Kafka version, select the latest available version\n4. Review and start the upgrade (Update/Start upgrade)\n5. Wait until the operation completes and the cluster status returns to Active",
"Terraform": "```hcl\n# Terraform: Upgrade MSK cluster to latest Kafka version\nresource \"aws_msk_cluster\" \"<example_resource_name>\" {\n cluster_name = \"<example_resource_name>\"\n kafka_version = \"<latest_version>\" # CRITICAL: set to the latest Kafka version to pass the check\n number_of_broker_nodes = 2\n\n broker_node_group_info {\n instance_type = \"kafka.m5.large\"\n client_subnets = [\"<example_resource_id>\", \"<example_resource_id>\"]\n\n storage_info {\n ebs_storage_info { volume_size = 1000 }\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "To upgrade your Amazon MSK cluster to the latest version, use the AWS Management Console, AWS CLI, or SDKs to update the cluster configuration. For more information, refer to the official Amazon MSK documentation.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/version-support.html#version-upgrades"
"Text": "Adopt a controlled upgrade strategy:\n- Track MSK version support and upgrade before end of support\n- Test in staging and schedule maintenance windows\n- Use blue/green or rolling upgrades to reduce downtime\n- Validate client compatibility and security settings\n- Consider serverless MSK if automatic versioning fits your risk model",
"Url": "https://hub.prowler.com/check/kafka_cluster_uses_latest_version"
}
},
"Categories": [],
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,28 +1,33 @@
{
"Provider": "aws",
"CheckID": "kafka_connector_in_transit_encryption_enabled",
"CheckTitle": "MSK Connect connectors should be encrypted in transit",
"CheckTitle": "MSK Connect connector has encryption in transit enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kafka",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:kafkaconnect:{region}:{account-id}:connector/{connector-name}/{connector-id}",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"Description": "This control checks whether an Amazon MSK Connect connector is encrypted in transit. This control fails if the connector isn't encrypted in transit.",
"Risk": "Data in transit can be intercepted or eavesdropped on by unauthorized users. Ensuring encryption in transit helps to protect sensitive data as it moves between nodes in a network or from your MSK cluster to connected applications.",
"RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-connect.html",
"Description": "**MSK Connect connectors** are evaluated for **in-transit encryption** using `TLS` on client connections to Kafka brokers and connected systems.",
"Risk": "Without **TLS**, data streams can be **intercepted** or **modified** in transit. Attackers on the path can perform **man-in-the-middle**, replay, or message **tampering**, exposing records and secrets. This degrades **confidentiality** and **integrity** and can enable unauthorized access to downstream systems.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/msk/latest/developerguide/msk-connect.html",
"https://docs.aws.amazon.com/msk/latest/developerguide/mkc-create-connector-intro.html"
],
"Remediation": {
"Code": {
"CLI": "aws kafkaconnect create-connector --encryption-in-transit-config 'EncryptionInTransitType=TLS'",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/msk-controls.html#msk-3",
"Terraform": ""
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: MSK Connect connector with in-transit encryption enabled\nResources:\n <example_resource_name>:\n Type: AWS::KafkaConnect::Connector\n Properties:\n ConnectorName: <example_resource_name>\n KafkaCluster:\n ApacheKafkaCluster:\n BootstrapServers: <BOOTSTRAP_SERVERS>\n Vpc:\n SecurityGroups: [<example_resource_id>]\n Subnets: [<example_resource_id>]\n KafkaClusterClientAuthentication:\n AuthenticationType: NONE\n KafkaClusterEncryptionInTransit:\n EncryptionType: TLS # Critical: enables TLS encryption in transit\n KafkaConnectVersion: <KAFKA_CONNECT_VERSION>\n Plugins:\n - CustomPlugin:\n CustomPluginArn: <example_resource_id>\n Revision: 1\n Capacity:\n ProvisionedCapacity:\n McuCount: 1\n WorkerCount: 1\n ServiceExecutionRoleArn: <example_resource_id>\n ConnectorConfiguration:\n connector.class: <CONNECTOR_CLASS>\n tasks.max: \"1\"\n```",
"Other": "1. In the AWS console, go to Amazon MSK > MSK Connect > Connectors\n2. Select the non-TLS connector and choose Delete (encryption setting can't be changed)\n3. Choose Create connector and select your custom plugin and cluster\n4. In the Security section, set Encryption in transit to TLS (required)\n5. Complete other required fields and Create the connector",
"Terraform": "```hcl\n# Terraform: MSK Connect connector with in-transit encryption enabled\nresource \"aws_mskconnect_connector\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n kafkaconnect_version = \"<KAFKA_CONNECT_VERSION>\"\n\n kafka_cluster {\n apache_kafka_cluster {\n bootstrap_servers = \"<BOOTSTRAP_SERVERS>\"\n vpc {\n security_groups = [\"<example_resource_id>\"]\n subnets = [\"<example_resource_id>\"]\n }\n }\n }\n\n kafka_cluster_client_authentication {\n authentication_type = \"NONE\"\n }\n\n kafka_cluster_encryption_in_transit {\n encryption_type = \"TLS\" # Critical: enables TLS encryption in transit\n }\n\n capacity {\n provisioned_capacity {\n mcu_count = 1\n worker_count = 1\n }\n }\n\n service_execution_role_arn = \"<example_resource_id>\"\n\n connector_configuration = {\n \"connector.class\" = \"<CONNECTOR_CLASS>\"\n \"tasks.max\" = \"1\"\n }\n\n plugin {\n custom_plugin {\n arn = \"<example_resource_id>\"\n revision = 1\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable encryption in transit for MSK Connect connectors to secure data as it moves across networks.",
"Url": "https://docs.aws.amazon.com/msk/latest/developerguide/mkc-create-connector-intro.html"
"Text": "Require **TLS** for all connector communications and disallow plaintext. Prefer private connectivity, validate certificates, and use modern cipher suites. Pair with **mutual authentication** and **least privilege** roles for defense-in-depth. Regularly review connector configs to avoid non-TLS endpoints.",
"Url": "https://hub.prowler.com/check/kafka_connector_in_transit_encryption_enabled"
}
},
"Categories": [
@@ -1,28 +1,32 @@
{
"Provider": "aws",
"CheckID": "kms_cmk_are_used",
"CheckTitle": "Check if there are CMK KMS keys not used.",
"CheckTitle": "KMS customer managed key is enabled or scheduled for deletion",
"CheckType": [
"Data Protection"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kms",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kms:region:account-id:certificate/resource-id",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "AwsKmsKey",
"Description": "Check if there are CMK KMS keys not used.",
"Risk": "Unused keys may increase service cost.",
"Description": "**Customer-managed KMS keys** are assessed by key state. Keys in `Enabled` are considered in use. Keys not `Enabled` and not `PendingDeletion` are identified as unused, while those in `PendingDeletion` are recognized as scheduled for removal.",
"Risk": "Keeping **unused CMKs** increases **attack surface** and **cost**.\n\nIf such keys are re-enabled or misconfigured, they can grant unintended decryption, impacting **confidentiality**. Deleting a key mistakenly thought unused can cause **irrecoverable data loss**, harming **availability**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys-determining-usage.html"
],
"Remediation": {
"Code": {
"CLI": "aws kms schedule-key-deletion --key-id <key_id> --pending-window-in-days 7",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "aws kms enable-key --key-id <key_id>",
"NativeIaC": "```yaml\n# CloudFormation: ensure the KMS CMK is enabled\nResources:\n <example_resource_name>:\n Type: AWS::KMS::Key\n Properties:\n Enabled: true # Critical: enables the key so its state is \"Enabled\" (PASS)\n KeyPolicy:\n Version: '2012-10-17'\n Statement:\n - Sid: Enable IAM User Permissions\n Effect: Allow\n Principal:\n AWS: !Sub arn:aws:iam::${AWS::AccountId}:root\n Action: 'kms:*'\n Resource: '*'\n```",
"Other": "1. Sign in to the AWS Console and open Key Management Service (KMS)\n2. Go to Customer managed keys and select the affected key\n3. Choose Key actions > Enable\n4. Confirm to enable the key",
"Terraform": "```hcl\n# Terraform: ensure the KMS CMK is enabled\nresource \"aws_kms_key\" \"<example_resource_name>\" {\n is_enabled = true # Critical: sets key state to Enabled (PASS)\n}\n```"
},
"Recommendation": {
"Text": "Before deleting a customer master key (CMK), you might want to know how many cipher-texts were encrypted under that key.",
"Url": "https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys-determining-usage.html"
"Text": "Adopt a **key lifecycle**: confirm actual usage with logs, owners, and tags; keep keys `Enabled` only when required; otherwise **schedule deletion** with a waiting period.\n\nEnforce **least privilege** to enable/disable or delete keys, require approvals, and monitor KMS activity with **separation of duties**.",
"Url": "https://hub.prowler.com/check/kms_cmk_are_used"
}
},
"Categories": [
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "kms_cmk_not_deleted_unintentionally",
"CheckTitle": "AWS KMS keys should not be deleted unintentionally",
"CheckTitle": "AWS KMS customer managed key is not scheduled for deletion",
"CheckType": [
"Data Deletion Protection"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Effects/Data Destruction"
],
"ServiceName": "kms",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kms:region:account-id:certificate/resource-id",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "AwsKmsKey",
"Description": "Ensure there is no customer keys scheduled for deletion.",
"Risk": "KMS keys cannot be recovered once deleted, also, all the data under a KMS key is also permanently unrecoverable if the KMS key is deleted.",
"RelatedUrl": "https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys-scheduling-key-deletion.html",
"Description": "**Customer-managed KMS keys** are evaluated for the `PendingDeletion` state, indicating a scheduled deletion during the mandatory waiting period.",
"Risk": "A key scheduled for deletion can lead to **permanent loss of decryption capability**, degrading **availability** and **integrity** of data and workloads. Accidental or malicious scheduling enables **cryptographic erasure**, causing outages, failed restores, and broken integrations during and after the wait window.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys-scheduling-key-deletion.html",
"https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys-scheduling-key-deletion.html#deleting-keys-scheduling-key-deletion-console"
],
"Remediation": {
"Code": {
"CLI": "aws kms cancel-key-deletion --key-id <key-id>",
"CLI": "aws kms cancel-key-deletion --key-id <KEY_ID>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/kms-controls.html#kms-3",
"Other": "1. Sign in to the AWS Management Console and open AWS KMS\n2. Go to Customer managed keys and select the key with status \"Pending deletion\"\n3. Click Key actions > Cancel key deletion\n4. Confirm to cancel; the key status will change from Pending deletion",
"Terraform": ""
},
"Recommendation": {
"Text": "Cancel the deletion before the end of the period unless you really want to delete that CMK, as it will no longer be usable.",
"Url": "https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys-scheduling-key-deletion.html#deleting-keys-scheduling-key-deletion-console"
"Text": "Prevent unintended deletion:\n- Enforce **least privilege** and **separation of duties** for key admins\n- Require change approvals and alerts on deletion events\n- Prefer **disabling** unused keys over deleting\n- Set sufficient waiting periods and review keys in `PendingDeletion` to verify authorization",
"Url": "https://hub.prowler.com/check/kms_cmk_not_deleted_unintentionally"
}
},
"Categories": [
@@ -1,31 +1,38 @@
{
"Provider": "aws",
"CheckID": "kms_cmk_not_multi_region",
"CheckTitle": "AWS KMS customer managed keys should not be multi-Region",
"CheckTitle": "AWS KMS customer managed key is single-Region",
"CheckType": [
"Data Protection"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "kms",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kms:region:account-id:key/resource-id",
"Severity": "high",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsKmsKey",
"Description": "Ensure that AWS KMS customer managed keys (CMKs) are not multi-region to maintain strict data control and compliance with security best practices.",
"Risk": "Multi-region KMS keys can increase the risk of unauthorized access and data exposure, as managing access controls and auditing across multiple regions becomes more complex. This expanded attack surface may lead to compliance violations and data breaches.",
"RelatedUrl": "https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html#multi-region-concepts",
"Description": "**AWS KMS customer-managed keys** in an `Enabled` state are assessed for the `multi-Region` setting. The finding highlights keys with the `multi-Region` property enabled.",
"Risk": "Shared key material across Regions lets access in one Region decrypt data from another, eroding **confidentiality** and **data residency**. A misconfigured policy or weaker controls in a replica expand the blast radius. For signing/HMAC keys, compromise enables cross-Region signature forgery, impacting **integrity** and **auditability**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html#multi-region-concepts",
"https://docs.aws.amazon.com/kms/latest/developerguide/mrk-when-to-use.html"
],
"Remediation": {
"Code": {
"CLI": "aws kms create-key --no-multi-region",
"NativeIaC": "",
"Other": "",
"Terraform": "resource \"aws_kms_key\" \"example\" { description = \"Single-region key\" multi_region = false }"
"CLI": "",
"NativeIaC": "```yaml\n# CloudFormation: create a single-Region KMS key\nResources:\n ExampleKmsKey:\n Type: AWS::KMS::Key\n Properties:\n MultiRegion: false # Critical: ensures the key is single-Region to pass the check\n KeyPolicy: # Minimal policy required for key creation\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n AWS: !Sub arn:aws:iam::${AWS::AccountId}:root\n Action: 'kms:*'\n Resource: '*'\n```",
"Other": "1. In the AWS Console, go to Key Management Service (KMS) > Customer managed keys\n2. Identify keys showing Multi-Region: Yes (these FAIL the check)\n3. Click Create key and ensure Multi-Region is not selected (single-Region)\n4. Update your services/aliases to use the new single-Region key\n5. Re-encrypt or rotate data to the new key where required\n6. After migration, disable the old multi-Region key and schedule its deletion",
"Terraform": "```hcl\n# Terraform: create a single-Region KMS key\nresource \"aws_kms_key\" \"example\" {\n multi_region = false # Critical: creates a single-Region key to pass the check\n}\n```"
},
"Recommendation": {
"Text": "Identify and replace multi-region keys with single-region KMS keys to enhance security and access control.",
"Url": "https://docs.aws.amazon.com/kms/latest/developerguide/mrk-when-to-use.html"
"Text": "Prefer **single-Region keys** by default; use **multi-Region** only with a documented need. Apply **least privilege** and **separation of duties**; limit who can create or replicate such keys. Isolate per Region/tenant/workload, standardize policy and logging across Regions, and retire multi-Region keys where unnecessary.",
"Url": "https://hub.prowler.com/check/kms_cmk_not_multi_region"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Multi-region keys should be used only when absolutely necessary, such as for cross-region disaster recovery, and should be carefully managed with strict access controls."
@@ -1,28 +1,34 @@
{
"Provider": "aws",
"CheckID": "kms_cmk_rotation_enabled",
"CheckTitle": "Ensure rotation for customer created KMS CMKs is enabled.",
"CheckTitle": "KMS customer-managed symmetric CMK has automatic rotation enabled",
"CheckType": [
"Data Protection"
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark"
],
"ServiceName": "kms",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kms:region:account-id:certificate/resource-id",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AwsKmsKey",
"Description": "Ensure rotation for customer created KMS CMKs is enabled.",
"Risk": "Cryptographic best practices discourage extensive reuse of encryption keys. Consequently, Customer Master Keys (CMKs) should be rotated to prevent usage of compromised keys.",
"RelatedUrl": "https://aws.amazon.com/blogs/security/how-to-get-ready-for-certificate-transparency/",
"Description": "**Customer-managed KMS symmetric keys** in the `Enabled` state are evaluated to confirm `automatic rotation` of key material is configured",
"Risk": "Without **automatic rotation**, long-lived key material increases confidentiality and integrity risk. If a KMS key is exposed, attackers can unwrap data keys and decrypt stored data until the key changes. It also reduces crypto agility and may conflict with mandated rotation policies.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html",
"https://aws.amazon.com/blogs/security/how-to-get-ready-for-certificate-transparency/"
],
"Remediation": {
"Code": {
"CLI": "aws kms enable-key-rotation --key-id <key_id>",
"NativeIaC": "",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-kms-have-rotation-policy#terraform"
"CLI": "aws kms enable-key-rotation --key-id <KEY_ID>",
"NativeIaC": "```yaml\n# CloudFormation: KMS key with automatic rotation enabled\nResources:\n <example_resource_name>:\n Type: AWS::KMS::Key\n Properties:\n EnableKeyRotation: true # Critical: enables automatic rotation so the key passes the check\n KeyPolicy:\n Version: \"2012-10-17\"\n Statement:\n - Effect: Allow\n Principal:\n AWS: !Sub arn:aws:iam::${AWS::AccountId}:root\n Action: \"kms:*\"\n Resource: \"*\"\n```",
"Other": "1. In the AWS Console, go to Key Management Service (KMS)\n2. Open Customer managed keys and select the enabled symmetric key\n3. Go to the Key rotation section\n4. Check Enable automatic key rotation\n5. Save changes",
"Terraform": "```hcl\n# KMS key with automatic rotation enabled\nresource \"aws_kms_key\" \"<example_resource_name>\" {\n enable_key_rotation = true # Critical: enables automatic rotation so the key passes the check\n}\n```"
},
"Recommendation": {
"Text": "For every KMS Customer Master Keys (CMKs), ensure that Rotate this key every year is enabled.",
"Url": "https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html"
"Text": "Enable **automatic rotation** on customer-managed symmetric KMS keys and choose a rotation period that meets policy. Enforce **least privilege** and **separation of duties** for key administration versus usage. Monitor key lifecycle events and use on-demand rotation when compromise is suspected.",
"Url": "https://hub.prowler.com/check/kms_cmk_rotation_enabled"
}
},
"Categories": [
@@ -1,33 +1,41 @@
{
"Provider": "aws",
"CheckID": "kms_key_not_publicly_accessible",
"CheckTitle": "Check exposed KMS keys",
"CheckTitle": "Cloud KMS key does not grant access to allUsers or allAuthenticatedUsers",
"CheckType": [
"Data Protection"
"Software and Configuration Checks/AWS Security Best Practices",
"Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"TTPs/Initial Access/Unauthorized Access",
"Effects/Data Exposure"
],
"ServiceName": "kms",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kms:region:account-id:certificate/resource-id",
"Severity": "medium",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "AwsKmsKey",
"Description": "Check exposed KMS keys",
"Risk": "Exposed KMS Keys or wide policy permissions my leave data unprotected.",
"RelatedUrl": "https://docs.aws.amazon.com/kms/latest/developerguide/determining-access.html",
"Description": "**KMS keys** are assessed for **excessive access** in key policies or grants, including `*` principals and broadly scoped permissions to multiple identities.",
"Risk": "Broad access to a **KMS key** enables unauthorized `kms:Decrypt` and data-key generation, breaking **confidentiality**. With admin rights, attackers can change policies or schedule deletion, undermining control **integrity** and threatening **availability** of data dependent on the key.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudKMS/publicly-accessible-kms-cryptokeys.html",
"https://support.icompaas.com/support/solutions/articles/62000232904-1-9-ensure-cloud-kms-cryptokeys-are-not-accessible-to-anonymous-or-public-users-automated-",
"https://docs.aws.amazon.com/kms/latest/developerguide/determining-access.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://github.com/cloudmatos/matos/tree/master/remediations/aws/kms/exposed-key",
"Terraform": ""
"CLI": "aws kms put-key-policy --key-id <example_resource_id> --policy-name default --policy '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::<account_id>:root\"},\"Action\":\"kms:*\",\"Resource\":\"*\"}]}'",
"NativeIaC": "```yaml\n# CloudFormation: restrict KMS key policy to account root (removes any public access)\nResources:\n <example_resource_name>:\n Type: AWS::KMS::Key\n Properties:\n KeyPolicy:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n AWS: arn:aws:iam::<account_id>:root # Critical: only account root can access; prevents public \"*\" principals\n Action: kms:*\n Resource: '*'\n```",
"Other": "1. Open AWS Console > Key Management Service (KMS)\n2. Select the affected key and go to the Key policy tab\n3. Click Edit and remove any statement with Principal set to \"*\" (or AWS: \"*\")\n4. Ensure a statement exists that allows only arn:aws:iam::<account_id>:root\n5. Save changes",
"Terraform": "```hcl\n# Restrict KMS key policy to the account root to avoid any public (\"*\") principals\ndata \"aws_caller_identity\" \"current\" {}\n\nresource \"aws_kms_key\" \"<example_resource_name>\" {\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [\n {\n Effect = \"Allow\"\n Principal = { AWS = \"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root\" } # Critical: limit to account root to remove public access\n Action = \"kms:*\"\n Resource = \"*\"\n }\n ]\n })\n}\n```"
},
"Recommendation": {
"Text": "To determine the full extent of who or what currently has access to a customer master key (CMK) in AWS KMS, you must examine the CMK key policy, all grants that apply to the CMK and potentially all AWS Identity and Access Management (IAM) policies. You might do this to determine the scope of potential usage of a CMK.",
"Url": "https://docs.aws.amazon.com/kms/latest/developerguide/determining-access.html"
"Text": "Apply **least privilege** to KMS keys:\n- Restrict principals to specific roles and accounts\n- Prefer narrow, time-bound grants\n- Separate key administration from usage\n- Use conditions to limit context\n- Review regularly and remove wildcard or cross-account exposure",
"Url": "https://hub.prowler.com/check/kms_key_not_publicly_accessible"
}
},
"Categories": [
"internet-exposed",
"encryption"
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],

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