Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acee366e82 | |||
| 47d66c9c4c | |||
| 8d41941d22 |
@@ -15,13 +15,6 @@ AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
|
||||
# Google Tag Manager ID
|
||||
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
|
||||
|
||||
#### MCP Server ####
|
||||
PROWLER_MCP_VERSION=stable
|
||||
# For UI and MCP running on docker:
|
||||
PROWLER_MCP_SERVER_URL=http://mcp-server:8000/mcp
|
||||
# For UI running on host, MCP in docker:
|
||||
# PROWLER_MCP_SERVER_URL=http://localhost:8000/mcp
|
||||
|
||||
#### Code Review Configuration ####
|
||||
# Enable Claude Code standards validation on pre-push hook
|
||||
# Set to 'true' to validate changes against AGENTS.md standards via Claude Code
|
||||
@@ -119,7 +112,7 @@ NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.16.1
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.12.2
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -32,6 +32,7 @@ Please add a detailed description of how to review this PR.
|
||||
#### API
|
||||
- [ ] Verify if API specs need to be regenerated.
|
||||
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
|
||||
- [ ] Query performance validated with `EXPLAIN ANALYZE` for new/modified endpoints. See [Query Performance Guide](https://github.com/prowler-cloud/prowler/blob/master/api/docs/query-performance-guide.md).
|
||||
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
|
||||
|
||||
### License
|
||||
|
||||
@@ -20,7 +20,6 @@ env:
|
||||
|
||||
jobs:
|
||||
api-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
@@ -44,7 +43,6 @@ jobs:
|
||||
ignore: DL3013
|
||||
|
||||
api-container-build-and-scan:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -92,7 +90,7 @@ jobs:
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Scan container with Trivy for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/trivy-scan
|
||||
with:
|
||||
image-name: ${{ env.IMAGE_NAME }}
|
||||
|
||||
@@ -20,7 +20,6 @@ env:
|
||||
|
||||
jobs:
|
||||
mcp-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
@@ -43,7 +42,6 @@ jobs:
|
||||
dockerfile: mcp_server/Dockerfile
|
||||
|
||||
mcp-container-build-and-scan:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -90,7 +88,7 @@ jobs:
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Scan MCP container with Trivy for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/trivy-scan
|
||||
with:
|
||||
image-name: ${{ env.IMAGE_NAME }}
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Scan SDK container with Trivy for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/trivy-scan
|
||||
with:
|
||||
image-name: ${{ env.IMAGE_NAME }}
|
||||
|
||||
@@ -20,7 +20,6 @@ env:
|
||||
|
||||
jobs:
|
||||
ui-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
@@ -44,7 +43,6 @@ jobs:
|
||||
ignore: DL3018
|
||||
|
||||
ui-container-build-and-scan:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -94,7 +92,7 @@ jobs:
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LwpXXXX
|
||||
|
||||
- name: Scan UI container with Trivy for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
if: github.repository == 'prowler-cloud/prowler' && steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/trivy-scan
|
||||
with:
|
||||
image-name: ${{ env.IMAGE_NAME }}
|
||||
|
||||
@@ -47,12 +47,12 @@ help: ## Show this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Build no cache
|
||||
build-no-cache-dev:
|
||||
docker compose -f docker-compose-dev.yml build --no-cache api-dev worker-dev worker-beat mcp-server
|
||||
build-no-cache-dev:
|
||||
docker compose -f docker-compose-dev.yml build --no-cache api-dev worker-dev worker-beat
|
||||
|
||||
##@ Development Environment
|
||||
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, MCP, and workers
|
||||
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat mcp-server
|
||||
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, and workers
|
||||
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat
|
||||
|
||||
##@ Development Environment
|
||||
build-and-run-api-dev: build-no-cache-dev run-api-dev
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<b><i>Prowler</b> is the Open Cloud Security platform trusted by thousands to automate security and compliance in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>Secure ANY cloud at AI Speed at <a href="https://prowler.com">prowler.com</i></b>
|
||||
<b>Learn more at <a href="https://prowler.com">prowler.com</i></b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -23,7 +23,6 @@
|
||||
<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>
|
||||
@@ -36,32 +35,28 @@
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center">
|
||||
<img align="center" src="/docs/img/prowler-cloud.gif" width="100%" height="100%">
|
||||
<img align="center" src="/docs/img/prowler-cli-quick.gif" width="100%" height="100%">
|
||||
</p>
|
||||
|
||||
# Description
|
||||
|
||||
**Prowler** is the world’s most widely used _open-source cloud security platform_ that automates security and compliance across **any cloud environment**. With hundreds of ready-to-use security checks, remediation guidance, and compliance frameworks, Prowler is built to _“Secure ANY cloud at AI Speed”_. Prowler delivers **AI-driven**, **customizable**, and **easy-to-use** assessments, dashboards, reports, and integrations, making cloud security **simple**, **scalable**, and **cost-effective** for organizations of any size.
|
||||
**Prowler** is an open-source security tool designed to assess and enforce security best practices across AWS, Azure, Google Cloud, and Kubernetes. It supports tasks such as security audits, incident response, continuous monitoring, system hardening, forensic readiness, and remediation processes.
|
||||
|
||||
Prowler includes hundreds of built-in controls to ensure compliance with standards and frameworks, including:
|
||||
|
||||
- **Prowler ThreatScore:** Weighted risk prioritization scoring that helps you focus on the most critical security findings first
|
||||
- **Industry Standards:** CIS, NIST 800, NIST CSF, CISA, and MITRE ATT&CK
|
||||
- **Regulatory Compliance and Governance:** RBI, FedRAMP, PCI-DSS, and NIS2
|
||||
- **Industry Standards:** CIS, NIST 800, NIST CSF, and CISA
|
||||
- **Regulatory Compliance and Governance:** RBI, FedRAMP, and PCI-DSS
|
||||
- **Frameworks for Sensitive Data and Privacy:** GDPR, HIPAA, and FFIEC
|
||||
- **Frameworks for Organizational Governance and Quality Control:** SOC2, GXP, and ISO 27001
|
||||
- **Cloud-Specific Frameworks:** AWS Foundational Technical Review (FTR), AWS Well-Architected Framework, and BSI C5
|
||||
- **National Security Standards:** ENS (Spanish National Security Scheme) and KISA ISMS-P (Korean)
|
||||
- **Frameworks for Organizational Governance and Quality Control:** SOC2 and GXP
|
||||
- **AWS-Specific Frameworks:** AWS Foundational Technical Review (FTR) and AWS Well-Architected Framework (Security Pillar)
|
||||
- **National Security Standards:** ENS (Spanish National Security Scheme)
|
||||
- **Custom Security Frameworks:** Tailored to your needs
|
||||
|
||||
## Prowler App / Prowler Cloud
|
||||
## Prowler App
|
||||
|
||||
Prowler App / [Prowler Cloud](https://cloud.prowler.com/) is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
|
||||
Prowler App is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
>For more details, refer to the [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
|
||||
|
||||
@@ -87,16 +82,16 @@ prowler dashboard
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface |
|
||||
|---|---|---|---|---|---|---|
|
||||
| AWS | 584 | 85 | 40 | 17 | Official | UI, API, CLI |
|
||||
| GCP | 89 | 17 | 14 | 5 | Official | UI, API, CLI |
|
||||
| Azure | 169 | 22 | 15 | 8 | Official | UI, API, CLI |
|
||||
| Kubernetes | 84 | 7 | 6 | 9 | Official | UI, API, CLI |
|
||||
| GitHub | 20 | 2 | 1 | 2 | Official | UI, API, CLI |
|
||||
| AWS | 576 | 82 | 39 | 10 | Official | UI, API, CLI |
|
||||
| GCP | 79 | 13 | 13 | 3 | Official | UI, API, CLI |
|
||||
| Azure | 162 | 19 | 13 | 4 | Official | UI, API, CLI |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 | Official | UI, API, CLI |
|
||||
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
|
||||
| M365 | 70 | 7 | 3 | 2 | Official | UI, API, CLI |
|
||||
| OCI | 52 | 15 | 1 | 12 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 63 | 10 | 1 | 9 | Official | CLI |
|
||||
| OCI | 51 | 13 | 1 | 10 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 61 | 9 | 1 | 9 | Official | CLI |
|
||||
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI |
|
||||
| MongoDB Atlas | 10 | 4 | 0 | 3 | Official | UI, API, CLI |
|
||||
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | UI, API, CLI |
|
||||
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
|
||||
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
|
||||
|
||||
@@ -277,12 +272,11 @@ python prowler-cli.py -v
|
||||
# ✏️ High level architecture
|
||||
|
||||
## Prowler App
|
||||
**Prowler App** is composed of four key components:
|
||||
**Prowler App** is composed of three key components:
|
||||
|
||||
- **Prowler UI**: A web-based interface, built with Next.js, providing a user-friendly experience for executing Prowler scans and visualizing results.
|
||||
- **Prowler API**: A backend service, developed with Django REST Framework, responsible for running Prowler scans and storing the generated results.
|
||||
- **Prowler SDK**: A Python SDK designed to extend the functionality of the Prowler CLI for advanced capabilities.
|
||||
- **Prowler MCP Server**: A Model Context Protocol server that provides AI tools for Lighthouse, the AI-powered security assistant. This is a critical dependency for Lighthouse functionality.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -2,30 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.17.0] (Prowler v5.16.0)
|
||||
|
||||
### 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)
|
||||
- Account id, alias and provider name to PDF reporting table [(#9574)](https://github.com/prowler-cloud/prowler/pull/9574)
|
||||
|
||||
### 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
|
||||
- Made `scan_id` a required filter in the compliance overview endpoint [(#9560)](https://github.com/prowler-cloud/prowler/pull/9560)
|
||||
- Reduced unnecessary UPDATE resources operations by only saving when tag mappings change, lowering write load during scans [(#9569)](https://github.com/prowler-cloud/prowler/pull/9569)
|
||||
|
||||
---
|
||||
|
||||
## [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)
|
||||
## [1.16.0] (Unreleased)
|
||||
|
||||
### Added
|
||||
- New endpoint to retrieve an overview of the attack surfaces [(#9309)](https://github.com/prowler-cloud/prowler/pull/9309)
|
||||
@@ -35,6 +12,7 @@ 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)
|
||||
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
# Query Performance Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to validate query performance when developing new endpoints or modifying existing ones. **This is part of the development process**, not a separate task—just like writing unit tests.
|
||||
|
||||
The goal is simple: ensure PostgreSQL uses indexes correctly for the queries your code generates.
|
||||
|
||||
## When to Validate
|
||||
|
||||
You **must** validate query performance when:
|
||||
|
||||
- Creating a new endpoint that queries the database
|
||||
- Modifying an existing query (adding filters, joins, or sorting)
|
||||
- Adding new indexes
|
||||
- Working on performance-critical endpoints (overviews, findings, resources)
|
||||
|
||||
## Profiling with Django Silk (Recommended)
|
||||
|
||||
[Django Silk](https://github.com/jazzband/django-silk) is the recommended way to profile queries because it captures the actual SQL generated by your code during real HTTP requests. This gives you the most accurate picture of what happens in production.
|
||||
|
||||
### Enabling Silk
|
||||
|
||||
Silk is installed as a dev dependency but disabled by default. To enable it temporarily for profiling:
|
||||
|
||||
#### 1. Add Silk to your local settings
|
||||
|
||||
In `api/src/backend/config/django/devel.py`, add at the end of the file:
|
||||
|
||||
```python
|
||||
# Silk profiler (temporary - remove after profiling)
|
||||
INSTALLED_APPS += ["silk"] # noqa: F405
|
||||
MIDDLEWARE += ["silk.middleware.SilkyMiddleware"] # noqa: F405
|
||||
```
|
||||
|
||||
#### 2. Add Silk URLs
|
||||
|
||||
In `api/src/backend/api/v1/urls.py`, add at the end:
|
||||
|
||||
```python
|
||||
from django.conf import settings
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))]
|
||||
```
|
||||
|
||||
#### 3. Run Silk migrations
|
||||
|
||||
```bash
|
||||
cd api/src/backend
|
||||
poetry run python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
#### 4. Access Silk
|
||||
|
||||
Start the development server and navigate to `http://localhost:8000/api/v1/silk/`
|
||||
|
||||
### Using Silk
|
||||
|
||||
1. Make requests to the endpoint you want to profile
|
||||
2. Open Silk UI and find your request
|
||||
3. Click on the request to see all SQL queries executed
|
||||
4. For each query, you can see:
|
||||
- Execution time
|
||||
- Number of similar queries (N+1 detection)
|
||||
- The actual SQL with parameters
|
||||
- **EXPLAIN output** (click on a query to see it)
|
||||
|
||||
### Disabling Silk
|
||||
|
||||
After profiling, **remove the changes** you made to `devel.py` and `urls.py`. Don't commit Silk configuration to the repository.
|
||||
|
||||
## Manual Query Analysis with EXPLAIN ANALYZE
|
||||
|
||||
For quick checks or when you need more control, you can run `EXPLAIN ANALYZE` directly.
|
||||
|
||||
### 1. Get Your Query
|
||||
|
||||
#### Option A: Using Django Shell with RLS
|
||||
|
||||
This approach mirrors how queries run in production with Row Level Security enabled:
|
||||
|
||||
```bash
|
||||
cd api/src/backend
|
||||
poetry run python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from django.db import connection
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Finding
|
||||
|
||||
tenant_id = "your-tenant-uuid"
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
# Build your queryset
|
||||
qs = Finding.objects.filter(status="FAIL").order_by("-inserted_at")[:25]
|
||||
|
||||
# Force evaluation
|
||||
list(qs)
|
||||
|
||||
# Get the SQL
|
||||
print(connection.queries[-1]['sql'])
|
||||
```
|
||||
|
||||
#### Option B: Print Query Without Execution
|
||||
|
||||
```python
|
||||
from api.models import Finding
|
||||
|
||||
queryset = Finding.objects.filter(status="FAIL")
|
||||
print(queryset.query)
|
||||
```
|
||||
|
||||
> **Note:** This won't include RLS filters, so the actual production query will differ.
|
||||
|
||||
#### Option C: Enable SQL Logging
|
||||
|
||||
Set `DJANGO_LOGGING_LEVEL=DEBUG` in your environment:
|
||||
|
||||
```bash
|
||||
DJANGO_LOGGING_LEVEL=DEBUG poetry run python manage.py runserver
|
||||
```
|
||||
|
||||
### 2. Run EXPLAIN ANALYZE
|
||||
|
||||
Connect to PostgreSQL and run:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE <your_query>;
|
||||
```
|
||||
|
||||
Or with more details:
|
||||
|
||||
```sql
|
||||
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) <your_query>;
|
||||
```
|
||||
|
||||
#### Running EXPLAIN with RLS Context
|
||||
|
||||
To test with RLS enabled (as it runs in production), set the tenant context first:
|
||||
|
||||
```sql
|
||||
-- Set tenant context
|
||||
SELECT set_config('api.tenant_id', 'your-tenant-uuid', TRUE);
|
||||
|
||||
-- Then run your EXPLAIN ANALYZE
|
||||
EXPLAIN ANALYZE SELECT * FROM findings WHERE status = 'FAIL' LIMIT 25;
|
||||
```
|
||||
|
||||
### 3. Interpret the Results
|
||||
|
||||
#### Good Signs (Index is being used)
|
||||
|
||||
```
|
||||
Index Scan using findings_tenant_status_idx on findings
|
||||
Index Cond: ((tenant_id = '...'::uuid) AND (status = 'FAIL'))
|
||||
Rows Removed by Filter: 0
|
||||
Actual Rows: 150
|
||||
Planning Time: 0.5 ms
|
||||
Execution Time: 2.3 ms
|
||||
```
|
||||
|
||||
#### Bad Signs (Sequential scan - no index)
|
||||
|
||||
```
|
||||
Seq Scan on findings
|
||||
Filter: ((tenant_id = '...'::uuid) AND (status = 'FAIL'))
|
||||
Rows Removed by Filter: 999850
|
||||
Actual Rows: 150
|
||||
Planning Time: 0.3 ms
|
||||
Execution Time: 450.2 ms
|
||||
```
|
||||
|
||||
## Quick Reference: What to Look For
|
||||
|
||||
| What You See | Meaning | Action |
|
||||
|--------------|---------|--------|
|
||||
| `Index Scan` | Index is being used | Good, no action needed |
|
||||
| `Index Only Scan` | Even better - data comes from index only | Good, no action needed |
|
||||
| `Bitmap Index Scan` | Index used, results combined | Usually fine |
|
||||
| `Seq Scan` on large tables | Full table scan, no index | **Needs investigation** |
|
||||
| `Rows Removed by Filter: <high number>` | Fetching too many rows | **Query or index issue** |
|
||||
| High `Execution Time` | Query is slow | **Needs optimization** |
|
||||
|
||||
## Common Issues and Fixes
|
||||
|
||||
### 1. Missing Index
|
||||
|
||||
**Problem:** `Seq Scan` on a filtered column
|
||||
|
||||
```sql
|
||||
-- Bad: No index on status
|
||||
EXPLAIN ANALYZE SELECT * FROM findings WHERE status = 'FAIL';
|
||||
-- Shows: Seq Scan on findings
|
||||
```
|
||||
|
||||
**Fix:** Add an index
|
||||
|
||||
```python
|
||||
# In your model
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['status'], name='findings_status_idx'),
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Index Not Used Due to Type Mismatch
|
||||
|
||||
**Problem:** Index exists but PostgreSQL doesn't use it
|
||||
|
||||
```sql
|
||||
-- If tenant_id is UUID but you're passing a string without cast
|
||||
WHERE tenant_id = 'some-uuid-string'
|
||||
```
|
||||
|
||||
**Fix:** Ensure proper type casting in your queries
|
||||
|
||||
### 3. Index Not Used Due to Function Call
|
||||
|
||||
**Problem:** Wrapping column in a function prevents index usage
|
||||
|
||||
```sql
|
||||
-- Bad: Index on inserted_at won't be used
|
||||
WHERE DATE(inserted_at) = '2024-01-01'
|
||||
|
||||
-- Good: Use range instead
|
||||
WHERE inserted_at >= '2024-01-01' AND inserted_at < '2024-01-02'
|
||||
```
|
||||
|
||||
### 4. Wrong Index for Sorting
|
||||
|
||||
**Problem:** Query is sorted but index doesn't match sort order
|
||||
|
||||
```sql
|
||||
-- If you have ORDER BY inserted_at DESC
|
||||
-- You need an index with DESC or PostgreSQL will sort in memory
|
||||
```
|
||||
|
||||
**Fix:** Create index with matching sort order
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['-inserted_at'], name='findings_inserted_desc_idx'),
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Composite Index Column Order
|
||||
|
||||
**Problem:** Index exists but columns are in wrong order
|
||||
|
||||
```sql
|
||||
-- Index on (tenant_id, scan_id)
|
||||
-- This query WON'T use the index efficiently:
|
||||
WHERE scan_id = '...'
|
||||
|
||||
-- This query WILL use the index:
|
||||
WHERE tenant_id = '...' AND scan_id = '...'
|
||||
```
|
||||
|
||||
**Rule:** The leftmost columns in a composite index must be in your WHERE clause.
|
||||
|
||||
## RLS (Row Level Security) Considerations
|
||||
|
||||
Prowler uses Row Level Security via PostgreSQL's `set_config`. When analyzing queries, remember:
|
||||
|
||||
1. RLS policies add implicit `WHERE tenant_id = current_tenant()` to queries
|
||||
2. Always test with RLS enabled (how it runs in production)
|
||||
3. Ensure `tenant_id` is the **first column** in composite indexes
|
||||
|
||||
### Using rls_transaction in Code
|
||||
|
||||
The `rls_transaction` context manager from `api.db_utils` sets the tenant context for all queries within its scope:
|
||||
|
||||
```python
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Finding
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
# All queries here will have RLS applied
|
||||
qs = Finding.objects.filter(status="FAIL")
|
||||
list(qs) # Execute
|
||||
```
|
||||
|
||||
### Using RLS in Raw SQL (psql)
|
||||
|
||||
```sql
|
||||
-- Set tenant context for the transaction
|
||||
SELECT set_config('api.tenant_id', 'your-tenant-uuid', TRUE);
|
||||
|
||||
-- Now RLS policies will filter by this tenant
|
||||
EXPLAIN ANALYZE SELECT * FROM findings WHERE status = 'FAIL';
|
||||
```
|
||||
|
||||
### Index Design for RLS
|
||||
|
||||
Since every query includes `tenant_id` via RLS, your composite indexes should **always start with `tenant_id`**:
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
indexes = [
|
||||
# Good: tenant_id first
|
||||
models.Index(fields=['tenant_id', 'status', 'severity']),
|
||||
|
||||
# Bad: tenant_id not first - RLS queries won't use this efficiently
|
||||
models.Index(fields=['status', 'tenant_id']),
|
||||
]
|
||||
```
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
The amount of test data you need depends on what you're testing. PostgreSQL's query planner considers table statistics, index definitions, and data distribution when choosing execution plans.
|
||||
|
||||
### Important Considerations
|
||||
|
||||
1. **Small datasets may not use indexes**: PostgreSQL may choose a sequential scan over an index scan if the table is small enough that scanning it directly is faster. This is expected behavior.
|
||||
|
||||
2. **Data must exist in the tables you're querying**: If your endpoint queries `findings`, `resources`, `scans`, or other tables, ensure those tables have data. Use the `findings` management command to generate test data:
|
||||
|
||||
```bash
|
||||
cd api/src/backend
|
||||
poetry run python manage.py findings \
|
||||
--tenant <TENANT_ID> \
|
||||
--findings 1000 \
|
||||
--resources 500 \
|
||||
--batch 500
|
||||
```
|
||||
|
||||
3. **Update table statistics**: After inserting test data, run `ANALYZE` to update PostgreSQL's statistics:
|
||||
|
||||
```sql
|
||||
ANALYZE findings;
|
||||
ANALYZE resources;
|
||||
ANALYZE scans;
|
||||
-- Add other tables as needed
|
||||
```
|
||||
|
||||
4. **Test with realistic data distribution**: If your query filters by a specific value (e.g., `status='FAIL'`), ensure your test data includes a realistic mix of values.
|
||||
|
||||
### When Index Usage Matters Most
|
||||
|
||||
Focus on validating index usage when:
|
||||
|
||||
- The table will have thousands or millions of rows in production
|
||||
- The query is called frequently (list endpoints, dashboards)
|
||||
- The query has multiple filters or joins
|
||||
|
||||
For small lookup tables or infrequently-called endpoints, sequential scans may be acceptable.
|
||||
|
||||
## Performance Checklist for PRs
|
||||
|
||||
Before submitting a PR that adds or modifies database queries:
|
||||
|
||||
- [ ] Profiled queries with Silk or `EXPLAIN ANALYZE`
|
||||
- [ ] Verified indexes are being used (no unexpected `Seq Scan` on large tables)
|
||||
- [ ] Checked `Rows Removed by Filter` is reasonable
|
||||
- [ ] Tested with RLS enabled
|
||||
- [ ] For critical endpoints: documented the query plan in the PR
|
||||
|
||||
## Useful Commands
|
||||
|
||||
### Update Table Statistics
|
||||
|
||||
```sql
|
||||
ANALYZE findings;
|
||||
ANALYZE resources;
|
||||
```
|
||||
|
||||
### See Existing Indexes
|
||||
|
||||
```sql
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'findings';
|
||||
```
|
||||
|
||||
### See Index Usage Stats
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE tablename = 'findings'
|
||||
ORDER BY idx_scan DESC;
|
||||
```
|
||||
|
||||
### Check Table Size
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
relname as table_name,
|
||||
pg_size_pretty(pg_total_relation_size(relid)) as total_size
|
||||
FROM pg_catalog.pg_statio_user_tables
|
||||
WHERE relname IN ('findings', 'resources', 'scans')
|
||||
ORDER BY pg_total_relation_size(relid) DESC;
|
||||
```
|
||||
|
||||
## Working with Partitioned Tables
|
||||
|
||||
The `findings` and `resource_finding_mappings` tables are partitioned. When adding indexes, use the helper functions from `api.db_utils`:
|
||||
|
||||
### Adding Indexes to Partitions
|
||||
|
||||
```python
|
||||
# In a migration file
|
||||
from functools import partial
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_utils import create_index_on_partitions, drop_index_on_partitions
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False # Required for CONCURRENTLY
|
||||
|
||||
dependencies = [
|
||||
("api", "XXXX_previous_migration"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="my_new_idx",
|
||||
columns="tenant_id, status, severity",
|
||||
all_partitions=False, # Only current/future partitions
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="my_new_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `all_partitions=False` (default): Only creates indexes on current and future partitions. Use this for new indexes to avoid maintenance overhead on old data.
|
||||
- `all_partitions=True`: Creates indexes on all partitions. Use when migrating critical existing indexes.
|
||||
|
||||
See [Partitions Documentation](./partitions.md) for more details on partitioning strategy.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Django Silk Documentation](https://github.com/jazzband/django-silk)
|
||||
- [PostgreSQL EXPLAIN Documentation](https://www.postgresql.org/docs/current/sql-explain.html)
|
||||
- [Using EXPLAIN](https://www.postgresql.org/docs/current/using-explain.html)
|
||||
- [Index Types in PostgreSQL](https://www.postgresql.org/docs/current/indexes-types.html)
|
||||
- [Prowler Partitions Documentation](./partitions.md)
|
||||
@@ -12,18 +12,6 @@ files = [
|
||||
{file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "24.1.0"
|
||||
description = "File support for asyncio."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
|
||||
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
@@ -160,480 +148,6 @@ files = [
|
||||
frozenlist = ">=1.1.0"
|
||||
typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-actiontrail20200706"
|
||||
version = "2.4.1"
|
||||
description = "Alibaba Cloud ActionTrail (20200706) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_actiontrail20200706-2.4.1-py3-none-any.whl", hash = "sha256:5dee0009db9b7cba182fbac742820f6a949287a8faafb843b5107f7dc89136da"},
|
||||
{file = "alibabacloud_actiontrail20200706-2.4.1.tar.gz", hash = "sha256:b65c6b37a96443fbe625dd5a4dd1be52a7476006a411db75206908b11588ffa8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-credentials"
|
||||
version = "1.0.3"
|
||||
description = "The alibabacloud credentials module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"},
|
||||
{file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiofiles = ">=22.1.0,<25.0.0"
|
||||
alibabacloud-credentials-api = ">=1.0.0,<2.0.0"
|
||||
alibabacloud-tea = ">=0.4.0"
|
||||
APScheduler = ">=3.10.0,<4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-credentials-api"
|
||||
version = "1.0.0"
|
||||
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-cs20151215"
|
||||
version = "6.1.0"
|
||||
description = "Alibaba Cloud CS (20151215) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_cs20151215-6.1.0-py3-none-any.whl", hash = "sha256:75e90b1bb9acca2236244bb0e44234ca4805d456ea4303ba4225ac15152a458e"},
|
||||
{file = "alibabacloud_cs20151215-6.1.0.tar.gz", hash = "sha256:5b3d99306701bf499ddd57cd9f2905b7721cb1bb4bb38ffe4d051f7b4e80e355"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-darabonba-array"
|
||||
version = "0.1.0"
|
||||
description = "Alibaba Cloud Darabonba Array SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_darabonba_array-0.1.0.tar.gz", hash = "sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-darabonba-encode-util"
|
||||
version = "0.0.2"
|
||||
description = "Darabonba Util Library for Alibaba Cloud Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_darabonba_encode_util-0.0.2.tar.gz", hash = "sha256:f1c484f276d60450fa49b4b2987194e741fcb2f7faae7f287c0ae65abc85fd4d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-darabonba-map"
|
||||
version = "0.0.1"
|
||||
description = "Alibaba Cloud Darabonba Map SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_darabonba_map-0.0.1.tar.gz", hash = "sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-darabonba-signature-util"
|
||||
version = "0.0.4"
|
||||
description = "Darabonba Util Library for Alibaba Cloud Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_darabonba_signature_util-0.0.4.tar.gz", hash = "sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-darabonba-string"
|
||||
version = "0.0.4"
|
||||
description = "Alibaba Cloud Darabonba String Library for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-darabonba-string-0.0.4.tar.gz", hash = "sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-darabonba-time"
|
||||
version = "0.0.1"
|
||||
description = "Alibaba Cloud Darabonba Time SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_darabonba_time-0.0.1.tar.gz", hash = "sha256:0ad9c7b0696570d1a3f40106cc7777f755fd92baa0d1dcab5b7df78dde5b922d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-ecs20140526"
|
||||
version = "7.2.5"
|
||||
description = "Alibaba Cloud Elastic Compute Service (20140526) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_ecs20140526-7.2.5-py3-none-any.whl", hash = "sha256:10bda5e185f6ba899e7d51477373595c629d66db7530a8a37433fb4e9034a96f"},
|
||||
{file = "alibabacloud_ecs20140526-7.2.5.tar.gz", hash = "sha256:2abbe630ce42d69061821f38950b938c5982cc31902ccd7132d05be328765a55"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-endpoint-util"
|
||||
version = "0.0.4"
|
||||
description = "The endpoint-util module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-gateway-oss"
|
||||
version = "0.0.17"
|
||||
description = "Alibaba Cloud OSS SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_gateway_oss-0.0.17.tar.gz", hash = "sha256:8c4b66c8c7dd285fc210ee232ab3f062b5573258752804d19382000746531e29"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud_credentials = ">=0.3.5"
|
||||
alibabacloud_darabonba_array = ">=0.1.0,<1.0.0"
|
||||
alibabacloud_darabonba_encode_util = ">=0.0.2,<1.0.0"
|
||||
alibabacloud_darabonba_map = ">=0.0.1,<1.0.0"
|
||||
alibabacloud_darabonba_signature_util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud_darabonba_string = ">=0.0.4,<1.0.0"
|
||||
alibabacloud_darabonba_time = ">=0.0.1,<1.0.0"
|
||||
alibabacloud_gateway_oss_util = ">=0.0.3,<1.0.0"
|
||||
alibabacloud_gateway_spi = ">=0.0.1,<1.0.0"
|
||||
alibabacloud_openapi_util = ">=0.2.1,<1.0.0"
|
||||
alibabacloud_oss_util = ">=0.0.5,<1.0.0"
|
||||
alibabacloud_tea_util = ">=0.3.11,<1.0.0"
|
||||
alibabacloud_tea_xml = ">=0.0.2,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-gateway-oss-util"
|
||||
version = "0.0.3"
|
||||
description = "Alibaba Cloud OSS Util Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_gateway_oss_util-0.0.3.tar.gz", hash = "sha256:5eb7fa450dc7350d5c71577974b9d7f489479e5c5ec7efc1c5376385e8c1c0a5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-gateway-sls"
|
||||
version = "0.4.0"
|
||||
description = "Alibaba Cloud SLS Gateway Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_gateway_sls-0.4.0-py3-none-any.whl", hash = "sha256:a0299a83a5528025983b42b7533a28028461bced5e180a66f97999e0134760a6"},
|
||||
{file = "alibabacloud_gateway_sls-0.4.0.tar.gz", hash = "sha256:9d2aceb377c9b3ed0558149fda16fe39fa114cc0a22e22a88dc76efdda34633b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-credentials = ">=1.0.2,<2.0.0"
|
||||
alibabacloud-darabonba-array = ">=0.1.0,<1.0.0"
|
||||
alibabacloud-darabonba-encode-util = ">=0.0.2,<1.0.0"
|
||||
alibabacloud-darabonba-map = ">=0.0.1,<1.0.0"
|
||||
alibabacloud-darabonba-signature-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-darabonba-string = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-gateway-sls-util = ">=0.4.0,<1.0.0"
|
||||
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-gateway-sls-util"
|
||||
version = "0.4.0"
|
||||
description = "Alibaba Cloud SLS Util Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_gateway_sls_util-0.4.0-py3-none-any.whl", hash = "sha256:c91ab7fe55af526a01d25b0d431088c4d241b160db055da3d8cb7330bd74595a"},
|
||||
{file = "alibabacloud_gateway_sls_util-0.4.0.tar.gz", hash = "sha256:f8b683a36a2ae3fe9a8225d3d97773ea769bdf9cdf4f4d033eab2eb6062ddd1f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aliyun-log-fastpb = ">=0.2.0"
|
||||
lz4 = ">=4.3.2"
|
||||
zstd = ">=1.5.5.1"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-gateway-spi"
|
||||
version = "0.0.3"
|
||||
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud_credentials = ">=0.3.4"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-openapi-util"
|
||||
version = "0.2.2"
|
||||
description = "Aliyun Tea OpenApi Library for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud_tea_util = ">=0.0.2"
|
||||
cryptography = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-oss-util"
|
||||
version = "0.0.6"
|
||||
description = "The oss util module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-tea = "*"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-oss20190517"
|
||||
version = "1.0.6"
|
||||
description = "Alibaba Cloud Object Storage Service (20190517) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_oss20190517-1.0.6-py3-none-any.whl", hash = "sha256:365fda353de6658a1a289f4d70dcd0394e2a8e2921b6b5834ba6d9772121d2f6"},
|
||||
{file = "alibabacloud_oss20190517-1.0.6.tar.gz", hash = "sha256:7cd0fb16af613ceb38d2e0e529aa1f58038c7cf59eb67c8c8775ae44ea717852"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-gateway-oss = ">=0.0.9,<1.0.0"
|
||||
alibabacloud-gateway-spi = ">=0.0.1,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.1,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.6,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.11,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-ram20150501"
|
||||
version = "1.2.0"
|
||||
description = "Alibaba Cloud Resource Access Management (20150501) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_ram20150501-1.2.0-py3-none-any.whl", hash = "sha256:03a0f2a0259848787c1f74e802b486184a88e04183486bd9398766971e5eb00a"},
|
||||
{file = "alibabacloud_ram20150501-1.2.0.tar.gz", hash = "sha256:6253513c8880769f4fd5b36fedddb362a9ca628ad9ae9c05c0eeacf5fbc95b42"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.15,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-rds20140815"
|
||||
version = "12.0.0"
|
||||
description = "Alibaba Cloud rds (20140815) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_rds20140815-12.0.0-py3-none-any.whl", hash = "sha256:0bd7e2018a428d86b1b0681087336e74665b48fc3eb0a13c4f4377ed5eab2b08"},
|
||||
{file = "alibabacloud_rds20140815-12.0.0.tar.gz", hash = "sha256:e7421d94f18a914c0a06b0e7fad0daff557713f1c97d415d463a78c1270e9b98"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.15,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-sas20181203"
|
||||
version = "6.1.0"
|
||||
description = "Alibaba Cloud Threat Detection (20181203) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_sas20181203-6.1.0-py3-none-any.whl", hash = "sha256:1ad735332c50c7961be036b17420d56b5ec3b5557e3aea1daa19491e8b75da20"},
|
||||
{file = "alibabacloud_sas20181203-6.1.0.tar.gz", hash = "sha256:e49ffd53e630274a8bf5a8299ca753023ad118510c80f6d9c6fb018b7479bf37"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-sls20201230"
|
||||
version = "5.9.0"
|
||||
description = "Alibaba Cloud Log Service (20201230) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_sls20201230-5.9.0-py3-none-any.whl", hash = "sha256:c4ae14096817a9686af5a0ae2389f1f6a8781e60b9edb8643445250cf15c26f1"},
|
||||
{file = "alibabacloud_sls20201230-5.9.0.tar.gz", hash = "sha256:bea830b64fbc7ed1719ba386ceeefb120f08d705f03eb0e02409dc6f12a291da"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-gateway-sls = ">=0.3.0,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-sts20150401"
|
||||
version = "1.1.6"
|
||||
description = "Alibaba Cloud Sts (20150401) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_sts20150401-1.1.6-py3-none-any.whl", hash = "sha256:627f5ca1f86e19b0bf8ce0e99071a36fb65579fad9256fbee38fdc8d500598e9"},
|
||||
{file = "alibabacloud_sts20150401-1.1.6.tar.gz", hash = "sha256:c2529b41e0e4531e21cb393e4df346e19fd6d54cc6337d1138dbcd2191438d4c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.15,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea"
|
||||
version = "0.4.3"
|
||||
description = "The tea module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.0,<4.0.0"
|
||||
requests = ">=2.21.0,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-openapi"
|
||||
version = "0.4.1"
|
||||
description = "Alibaba Cloud openapi SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_tea_openapi-0.4.1-py3-none-any.whl", hash = "sha256:e46bfa3ca34086d2c357d217a0b7284ecbd4b3bab5c88e075e73aec637b0e4a0"},
|
||||
{file = "alibabacloud_tea_openapi-0.4.1.tar.gz", hash = "sha256:2384b090870fdb089c3c40f3fb8cf0145b8c7d6c14abbac521f86a01abb5edaf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-credentials = ">=1.0.2,<2.0.0"
|
||||
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
cryptography = ">=3.0.0,<45.0.0"
|
||||
darabonba-core = ">=1.0.3,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-util"
|
||||
version = "0.3.14"
|
||||
description = "The tea-util module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"},
|
||||
{file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-tea = ">=0.3.3"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-xml"
|
||||
version = "0.0.3"
|
||||
description = "The tea-xml module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-tea = ">=0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-vpc20160428"
|
||||
version = "6.13.0"
|
||||
description = "Alibaba Cloud Virtual Private Cloud (20160428) SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_vpc20160428-6.13.0-py3-none-any.whl", hash = "sha256:933cf1e74322a20a2df27ca6323760d857744a4246eeadc9fb3eae01322fb1c6"},
|
||||
{file = "alibabacloud_vpc20160428-6.13.0.tar.gz", hash = "sha256:daf00679a83d422799f9fcf263739fe1f360641675843cbfbe623833fc8b1681"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
|
||||
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
|
||||
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alive-progress"
|
||||
version = "3.3.0"
|
||||
@@ -650,32 +164,6 @@ files = [
|
||||
about-time = "4.2.1"
|
||||
graphemeu = "0.7.2"
|
||||
|
||||
[[package]]
|
||||
name = "aliyun-log-fastpb"
|
||||
version = "0.2.0"
|
||||
description = "Fast protobuf serialization for Aliyun Log using PyO3 and quick-protobuf"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:51633d92d2b349aed4843c0b503454fb4f7d73eeaaa54f82aa5a36c10c064ef5"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2984aafc61ccbbf1db2589ce90b6d5a26e72dba137fb1fdf7f61ce3faa967c0"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181fc61ac9934f58b0880fa5617a4a4dc709dba09f8be95b5a71e828f2e48053"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12b8bfddf0bc5450f16f1954c6387a73da124fae10d1205a17a0117e66bb56db"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fbc83cbaa51d332e5e68871c1200014f1f3de54a8cba4fb55a634ee145cd4e4"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a86a6e11dd227d595fa23f69d30588446af19d045d1003bd1b66b5c9a55485"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd92c0b84ba300c1d1c227204c5f2fff243cea80bc3f9399293385e87c82ee3e"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c07a6d81a3eab6666949240da305236ed2350c305154d7e39fcc121fc52291"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cff4fbdd0edff94adcee1dcabf16daacb5d336a12fc897887aa6e4f0ad25152"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5a451809e2a062accbb8dae8750e507e58806e4a8da48d69215cdeef428e9d63"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:61f09df30232f1f5628d13310cf0e175171399ea1c75a8470e9f9d97b045bfb5"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:a5fbf0d41d8c0c964a3dc8dd0ee2e732f876b803e0ed3432550ef3b84dde84f1"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ae2f84ed0777e00045791044a56413f370afbd5b061505f5ded540c04b19c58e"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win32.whl", hash = "sha256:967f9656c805602fd9be07d8c2756ad89204c852c99689c3c71aa035416ef42a"},
|
||||
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:bbdcf7b85f0f3437c2a8e8a1db0ef5584d21468b7c7a358269a4c651c84f4a54"},
|
||||
{file = "aliyun_log_fastpb-0.2.0.tar.gz", hash = "sha256:91c714e76fb941c9a0db6b1aa1f4c56cb1626254ff5444c1179860f5e5b63d93"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "amqp"
|
||||
version = "5.3.1"
|
||||
@@ -723,34 +211,6 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
[package.extras]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "apscheduler"
|
||||
version = "3.11.1"
|
||||
description = "In-process task scheduler with Cron-like capabilities"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2"},
|
||||
{file = "apscheduler-3.11.1.tar.gz", hash = "sha256:0db77af6400c84d1747fe98a04b8b58f0080c77d11d338c4f507a9752880f221"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tzlocal = ">=3.0"
|
||||
|
||||
[package.extras]
|
||||
doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"]
|
||||
etcd = ["etcd3", "protobuf (<=3.21.0)"]
|
||||
gevent = ["gevent"]
|
||||
mongodb = ["pymongo (>=3.0)"]
|
||||
redis = ["redis (>=3.0)"]
|
||||
rethinkdb = ["rethinkdb (>=2.4.0)"]
|
||||
sqlalchemy = ["sqlalchemy (>=1.4)"]
|
||||
test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytz", "twisted ; python_version < \"3.14\""]
|
||||
tornado = ["tornado (>=4.3)"]
|
||||
twisted = ["twisted"]
|
||||
zookeeper = ["kazoo"]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.9.1"
|
||||
@@ -2068,22 +1528,6 @@ files = [
|
||||
docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
|
||||
tests = ["pytest", "pytest-cov", "pytest-xdist"]
|
||||
|
||||
[[package]]
|
||||
name = "darabonba-core"
|
||||
version = "1.0.5"
|
||||
description = "The darabonba module of alibabaCloud Python SDK."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.0,<4.0.0"
|
||||
alibabacloud-tea = "*"
|
||||
requests = ">=2.21.0,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "dash"
|
||||
version = "3.1.1"
|
||||
@@ -4039,78 +3483,6 @@ html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=3.0.11,<3.1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "lz4"
|
||||
version = "4.4.5"
|
||||
description = "LZ4 Bindings for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd"},
|
||||
{file = "lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004"},
|
||||
{file = "lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33"},
|
||||
{file = "lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22"},
|
||||
{file = "lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb"},
|
||||
{file = "lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be"},
|
||||
{file = "lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-win32.whl", hash = "sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7"},
|
||||
{file = "lz4-4.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0"},
|
||||
{file = "lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.0)", "sphinx_bootstrap_theme"]
|
||||
flake8 = ["flake8"]
|
||||
tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.9"
|
||||
@@ -5408,7 +4780,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.16.0"
|
||||
version = "5.14.0"
|
||||
description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks."
|
||||
optional = false
|
||||
python-versions = ">3.9.1,<3.13"
|
||||
@@ -5417,19 +4789,6 @@ files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud_actiontrail20200706 = "2.4.1"
|
||||
alibabacloud_credentials = "1.0.3"
|
||||
alibabacloud_cs20151215 = "6.1.0"
|
||||
alibabacloud_ecs20140526 = "7.2.5"
|
||||
alibabacloud-gateway-oss-util = "0.0.3"
|
||||
alibabacloud_oss20190517 = "1.0.6"
|
||||
alibabacloud_ram20150501 = "1.2.0"
|
||||
alibabacloud-rds20140815 = "12.0.0"
|
||||
alibabacloud_sas20181203 = "6.1.0"
|
||||
alibabacloud-sls20201230 = "5.9.0"
|
||||
alibabacloud_sts20150401 = "1.1.6"
|
||||
alibabacloud_tea_openapi = "0.4.1"
|
||||
alibabacloud_vpc20160428 = "6.13.0"
|
||||
alive-progress = "3.3.0"
|
||||
awsipranges = "0.3.3"
|
||||
azure-identity = "1.21.0"
|
||||
@@ -5493,8 +4852,8 @@ tzlocal = "5.3.1"
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/prowler-cloud/prowler.git"
|
||||
reference = "v5.16"
|
||||
resolved_reference = "f0e59bcb13383d7bb1aa9804906ac99aed820a09"
|
||||
reference = "master"
|
||||
resolved_reference = "de5aba6d4db54eed4c95cb7629443da186c17afd"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
@@ -6706,7 +6065,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
|
||||
@@ -6715,7 +6073,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
|
||||
@@ -6724,7 +6081,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
|
||||
@@ -6733,7 +6089,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
|
||||
@@ -6742,7 +6097,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
|
||||
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
|
||||
@@ -7707,143 +7061,7 @@ docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"]
|
||||
test = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "1.5.7.2"
|
||||
description = "ZSTD Bindings for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "zstd-1.5.7.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e17104d0e88367a7571dde4286e233126c8551691ceff11f9ae2e3a3ac1bb483"},
|
||||
{file = "zstd-1.5.7.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d6ee5dfada4c8fa32f43cc092fcf7d8482da6ad242c22fdf780f7eebd0febcc7"},
|
||||
{file = "zstd-1.5.7.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:ae1100776cb400100e2d2f427b50dc983c005c38cd59502eb56d2cfea3402ad5"},
|
||||
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:489a0ff15caf7640851e63f85b680c4279c99094cd500a29c7ed3ab82505fce0"},
|
||||
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:92590cf54318849d492445c885f1a42b9dbb47cdc070659c7cb61df6e8531047"},
|
||||
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux_2_4_i686.whl", hash = "sha256:2bc21650f7b9c058a3c4cb503e906fe9cce293941ec1b48bc5d005c3b4422b42"},
|
||||
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux_2_4_x86_64.whl", hash = "sha256:7b13e7eef9aa192804d38bf413924d347c6f6c6ac07f5a0c1ae4a6d7b3af70f0"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3f14c5c405ea353b68fe105236780494eb67c756ecd346fd295498f5eab6d24"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07d2061df22a3efc06453089e6e8b96e58f5bb7a0c4074dcfd0b0ce243ddde72"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:27e55aa2043ba7d8a08aba0978c652d4d5857338a8188aa84522569f3586c7bb"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e97933addfd71ea9608306f18dc18e7d2a5e64212ba2bb9a4ccb6d714f9f280"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_4_i686.whl", hash = "sha256:27e2ed58b64001c9ef0a8e028625477f1a6ed4ca949412ff6548544945cc59c2"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_4_x86_64.whl", hash = "sha256:92f072819fc0c7e8445f51a232c9ad76642027c069d2f36470cdb5e663839cdb"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2a653cdd2c52d60c28e519d44bde8d759f2c1837f0ff8e8e1b0045ca62fcf70e"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:047803d87d910f4905f48d99aeff1e0539ec2e4f4bf17d077701b5d0b2392a95"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0d8c1dc947e5ccea3bd81043080213685faf1d43886c27c51851fabf325f05c0"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8291d393321fac30604c6bbf40067103fee315aa476647a5eaecf877ee53496f"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-win32.whl", hash = "sha256:6922ceac5f2d60bb57a7875168c8aa442477b83e8951f2206cf1e9be788b0a6e"},
|
||||
{file = "zstd-1.5.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:346d1e4774d89a77d67fc70d53964bfca57c0abecfd885a4e00f87fd7c71e074"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f799c1e9900ad77e7a3d994b9b5146d7cfd1cbd1b61c3db53a697bf21ffcc57b"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ff4c667f29101566a7b71f06bbd677a63192818396003354131f586383db042"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8526a32fa9f67b07fd09e62474e345f8ca1daf3e37a41137643d45bd1bc90773"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2cec2472760d48a7a3445beaba509d3f7850e200fed65db15a1a66e315baec6a"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:a200c479ee1bb661bc45518e016a1fdc215a1d8f7e4bf6c7de0af254976cfdf6"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_4_x86_64.whl", hash = "sha256:f5d159e57a13147aa8293c0f14803a75e9039fd8afdf6cf1c8c2289fb4d2333a"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:7206934a2bd390080e972a1fed5a897e184dfd71dbb54e978dc11c6b295e1806"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e0027b20f296d1c9a8e85b8436834cf46560240a29d623aa8eaa8911832eb58"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d6b17e5581dd1a13437079bd62838d2635db8eb8aca9c0e9251faa5d4d40a6d7"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b13285c99cc710f60dd270785ec75233018870a1831f5655d862745470a0ca29"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-win32.whl", hash = "sha256:cdb5ec80da299f63f8aeccec0bff3247e96252d4c8442876363ff1b438d8049b"},
|
||||
{file = "zstd-1.5.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:4f6861c8edceb25fda37cdaf422fc5f15dcc88ced37c6a5b3c9011eda51aa218"},
|
||||
{file = "zstd-1.5.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ebe3e60dbace52525fa7aa604479e231dc3e4fcc76d0b4c54d8abce5e58734"},
|
||||
{file = "zstd-1.5.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef201b6f7d3a6751d85cc52f9e6198d4d870e83d490172016b64a6dd654a9583"},
|
||||
{file = "zstd-1.5.7.2-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:ac7bdfedda51b1fcdcf0ab69267d01256fc97ddf666ce894fde0fae9f3630eac"},
|
||||
{file = "zstd-1.5.7.2-cp312-cp312-manylinux_2_4_i686.whl", hash = "sha256:b835405cc4080b378e45029f2fe500e408d1eaedfba7dd7402aba27af16955f9"},
|
||||
{file = "zstd-1.5.7.2-cp312-cp312-win32.whl", hash = "sha256:e4cf97bb97ed6dbb62d139d68fd42fa1af51fd26fd178c501f7b62040e897c50"},
|
||||
{file = "zstd-1.5.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:55e2edc4560a5cf8ee9908595e90a15b1f47536ea9aad4b2889f0e6165890a38"},
|
||||
{file = "zstd-1.5.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e684e27064b6550aa2e7dc85d171ea1b62cb5930a2c99b3df9b30bf620b5c06"},
|
||||
{file = "zstd-1.5.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd6262788a98807d6b2befd065d127db177c1cd76bb8e536e0dded419eb7c7fb"},
|
||||
{file = "zstd-1.5.7.2-cp313-cp313-manylinux_2_14_x86_64.whl", hash = "sha256:53948be45f286a1b25c07a6aa2aca5c902208eb3df9fe36cf891efa0394c8b71"},
|
||||
{file = "zstd-1.5.7.2-cp313-cp313-win32.whl", hash = "sha256:edf816c218e5978033b7bb47dcb453dfb71038cb8a9bf4877f3f823e74d58174"},
|
||||
{file = "zstd-1.5.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:eea9bddf06f3f5e1e450fd647665c86df048a45e8b956d53522387c1dff41b7a"},
|
||||
{file = "zstd-1.5.7.2-cp313-cp313t-manylinux_2_14_x86_64.whl", hash = "sha256:1d71f9f92b3abe18b06b5f0aefa5b9c42112beef3bff27e36028d147cb4426a6"},
|
||||
{file = "zstd-1.5.7.2-cp314-cp314-manylinux_2_14_x86_64.whl", hash = "sha256:a6105b8fa21dbc59e05b6113e8e5d5aaf56c5d2886aa5778d61030af3256bbb7"},
|
||||
{file = "zstd-1.5.7.2-cp314-cp314t-manylinux_2_14_x86_64.whl", hash = "sha256:d0b0ca097efb5f67157c61a744c926848dcccf6e913df2f814e719aa78197a4b"},
|
||||
{file = "zstd-1.5.7.2-cp34-cp34m-manylinux_2_4_i686.whl", hash = "sha256:a371274668182ae06be2e321089b207fa0a75a58ae2fd4dfb7eafded9e041b2f"},
|
||||
{file = "zstd-1.5.7.2-cp34-cp34m-manylinux_2_4_x86_64.whl", hash = "sha256:74c3f006c9a3a191ed454183f0fb78172444f5cb431be04d85044a27f1b58c7b"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f19a3e658d92b6b52020c4c6d4c159480bcd3b47658773ea0e8d343cee849f33"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d9d1bcb6441841c599883139c1b0e47bddb262cce04b37dc2c817da5802c1158"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:bb1cb423fc40468cc9b7ab51a5b33c618eefd2c910a5bffed6ed76fe1cbb20b0"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux_2_14_x86_64.whl", hash = "sha256:e2476ba12597e58c5fc7a3ae547ee1bef9dd6b9d5ea80cf8d4034930c5a336e0"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux_2_4_i686.whl", hash = "sha256:2bf6447373782a2a9df3015121715f6d0b80a49a884c2d7d4518c9571e9fca16"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-win32.whl", hash = "sha256:a59a136a9eaa1849d715c004e30344177e85ad6e7bc4a5d0b6ad2495c5402675"},
|
||||
{file = "zstd-1.5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:114115af8c68772a3205414597f626b604c7879f6662a2a79c88312e0f50361f"},
|
||||
{file = "zstd-1.5.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f576ec00e99db124309dac1e1f34bc320eb69624189f5fdaf9ebe1dc81581a84"},
|
||||
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f97d8593da0e23a47f148a1cb33300dccd513fb0df9f7911c274e228a8c1a300"},
|
||||
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:a130243e875de5aeda6099d12b11bc2fcf548dce618cf6b17f731336ba5338e4"},
|
||||
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:73cec37649fda383348dc8b3b5fba535f1dbb1bbaeb60fd36f4c145820208619"},
|
||||
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux_2_14_x86_64.whl", hash = "sha256:883e7b77a3124011b8badd0c7c9402af3884700a3431d07877972e157d85afb8"},
|
||||
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux_2_4_i686.whl", hash = "sha256:b5af6aa041b5515934afef2ef4af08566850875c3c890109088eedbe190eeefb"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:53abf577aec7b30afa3c024143f4866676397c846b44f1b30d8097b5e4f5c7d7"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:660945ba16c16957c94dafc40aff1db02a57af0489aa3a896866239d47bb44b0"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3e220d2d7005822bb72a52e76410ca4634f941d8062c08e8e3285733c63b1db7"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_4_i686.whl", hash = "sha256:7e998f86a9d1e576c0158bf0b0a6a5c4685679d74ba0053a2e87f684f9bdc8eb"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_4_x86_64.whl", hash = "sha256:70d0c4324549073e05aa72e9eb6a593f89cba59da804b946d325d68467b93ad5"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:b9518caabf59405eddd667bbb161d9ae7f13dbf96967fd998d095589c8d41c86"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:30d339d8e5c4b14c2015b50371fcdb8a93b451ca6d3ef813269ccbb8b3b3ef7d"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:6f5539a10b838ee576084870eed65b63c13845e30a5b552cfe40f7e6b621e61a"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:5540ce1c99fa0b59dad2eff771deb33872754000da875be50ac8c2beab42b433"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-win32.whl", hash = "sha256:56c4b8cd0a88fd721213661c28b87b64fbd14b6019df39b21b0117a68162b0f2"},
|
||||
{file = "zstd-1.5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:594f256fa72852ade60e3acb909f983d5cf6839b9fc79728dd4b48b31112058f"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dc05618eb0abceb296b77e5f608669c12abc69cbf447d08151bcb14d290ab07"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70231ba799d681b6fc17456c3e39895c493b5dff400aa7842166322a952b7f2a"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5a73f0f20f71d4eef970a3fed7baac64d9a2a00b238acc4eca2bd7172bd7effb"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0a470f8938f69f632b8f88b96578a5e8825c18ddbbea7de63493f74874f963ef"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_4_i686.whl", hash = "sha256:d104f1cb2a7c142007c29a2a62dfe633155c648317a465674e583c295e5f792d"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_4_x86_64.whl", hash = "sha256:70f29e0504fc511d4b9f921e69637fca79c050e618ba23732a3f75c044814d89"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a62c2f6f7b8fc69767392084828740bd6faf35ff54d4ccb2e90e199327c64140"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2dda0c76f87723fb7f75d7ad3bbd90f7fb47b75051978d22535099325111b41"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f9cf09c2aa6f67750fe9f33fdd122f021b1a23bf7326064a8e21f7af7e77faee"},
|
||||
{file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:910bd9eac2488439f597504756b03c74aa63ed71b21e5d0aa2c7e249b3f1c13f"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9838ec7eb9f1beb2f611b9bcac7a169cb3de708ccf779aead29787e4482fe232"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:83a36bb1fd574422a77b36ccf3315ab687aef9a802b0c3312ca7006b74eeb109"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6f8189bc58415758bbbd419695012194f5e5e22c34553712d9a3eb009c09808d"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:632e3c1b7e1ebb0580f6d92b781a8f7901d367cf72725d5642e6d3a32e404e45"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_4_i686.whl", hash = "sha256:df8083c40fdbfe970324f743f0b5ecc244c37736e5f3ad2670de61dde5e0b024"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_4_x86_64.whl", hash = "sha256:300db1ede4d10f8b9b3b99ca52b22f0e2303dc4f1cf6994d1f8345ce22dd5a7e"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:97b908ccb385047b0c020ce3dc55e6f51078c9790722fdb3620c076be4a69ecf"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c59218bd36a7431a40591504f299de836ea0d63bc68ea76d58c4cf5262f0fa3c"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4d5a85344193ec967d05da8e2c10aed400e2d83e16041d2fdfb713cfc8caceeb"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebf6c1d7f0ceb0af5a383d2a1edc8ab9ace655e62a41c8a4ed5a031ee2ef8006"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-win32.whl", hash = "sha256:44a5142123d59a0dbbd9ba9720c23521be57edbc24202223a5e17405c3bdd4a6"},
|
||||
{file = "zstd-1.5.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dc542a9818712a9fb37563fa88cdbbbb2b5f8733111d412b718fa602b83ba45"},
|
||||
{file = "zstd-1.5.7.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:24371a7b0475eef7d933c72067d363c5dc17282d2aa5d4f5837774378718509e"},
|
||||
{file = "zstd-1.5.7.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:c21d44981b068551f13097be3809fadb7f81617d0c21b2c28a7d04653dde958f"},
|
||||
{file = "zstd-1.5.7.2-pp27-pypy_73-manylinux_2_14_x86_64.whl", hash = "sha256:b011bf4cfad78cdf9116d6731234ff181deb9560645ffdcc8d54861ae5d1edfc"},
|
||||
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:426e5c6b7b3e2401b734bfd08050b071e17c15df5e3b31e63651d1fd9ba4c751"},
|
||||
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:53375b23f2f39359ade944169bbd88f8895eed91290ee608ccbc28810ac360ba"},
|
||||
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:1b301b2f9dbb0e848093127fb10cbe6334a697dc3aea6740f0bb726450ee9a34"},
|
||||
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5414c9ae27069ab3ec8420fe8d005cb1b227806cbc874a7b4c73a96b4697a633"},
|
||||
{file = "zstd-1.5.7.2-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:5fb2ff5718fe89181223c23ce7308bd0b4a427239379e2566294da805d8df68a"},
|
||||
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:9714d5642867fceb22e4ab74aebf81a2e62dc9206184d603cb39277b752d5885"},
|
||||
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:6584fd081a6e7d92dffa8e7373d1fced6b3cbf473154b82c17a99438c5e1de51"},
|
||||
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:52f27a198e2a72632bae12ec63ebaa31b10e3d5f3dd3df2e01376979b168e2e6"},
|
||||
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:3b14793d2a2cb3a7ddd1cf083321b662dd20bc11143abc719456e9bfd22a32aa"},
|
||||
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:faf3fd38ba26167c5a085c04b8c931a216f1baf072709db7a38e61dea52e316e"},
|
||||
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:d17ac6d2584168247796174e599d4adbee00153246287e68881efaf8d48a6970"},
|
||||
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9a24d492c63555b55e6bc73a9e82a38bf7c3e8f7cde600f079210ed19cb061f2"},
|
||||
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c6abf4ab9a9d1feb14bc3cbcc32d723d340ce43b79b1812805916f3ac069b073"},
|
||||
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d7131bb4e55d075cb7847555a1e17fca5b816a550c9b9ac260c01799b6f8e8d9"},
|
||||
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a03608499794148f39c932c508d4eb3622e79ca2411b1d0438a2ee8cafdc0111"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:86e64c71b4d00bf28be50e4941586e7874bdfa74858274d9f7571dd5dda92086"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0f79492bf86aef6e594b11e29c5589ddd13253db3ada0c7a14fb176b132fb65e"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:8c3f4bb8508bc54c00532931da4a5261f08493363da14a5526c986765973e35d"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:787bcf55cefc08d27aca34c6dcaae1a24940963d1a73d4cec894ee458c541ac4"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f97f872cb78a4fd60b6c1024a65a4c52a971e9d991f33c7acd833ee73050f85"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:5e530b75452fdcff4ea67268d9e7cb37a38e7abbac84fa845205f0b36da81aaf"},
|
||||
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7c1cc65fc2789dd97a98202df840537de186ed04fd1804a17fcb15d1232442c4"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:05604a693fa53b60ca083992324b08dafd15a4ac37ac4cffe4b43b9eb93d4440"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:baf4e8b46d8934d4e85373f303eb048c63897fc4191d8ab301a1bbdf30b7a3cc"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:8cc35cc25e2d4a0f68020f05cba96912a2881ebaca890d990abe37aa3aa27045"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:ceae57e369e1b821b8f2b4c59bc08acd27d8e4bf9687bfa5211bc4cdb080fe7b"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5189fb44c44ab9b6c45f734bd7093a67686193110dc90dcfaf0e3a31b2385f38"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:f51a965871b25911e06d421212f9be7f7bcd3cedc43ea441a8a73fad9952baa0"},
|
||||
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:624022851c51dd6d6b31dbfd793347c4bd6339095e8383e2f74faf4f990b04c6"},
|
||||
{file = "zstd-1.5.7.2.tar.gz", hash = "sha256:6d8684c69009be49e1b18ec251a5eb0d7e24f93624990a8a124a1da66a92fc8a"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "c3f69105de7e604d4978c53877203d69c59d22276e8d7c751f4960764a5f926c"
|
||||
content-hash = "77ef098291cb8631565a1ab5027ce33e7fcb5a04883dc7160bf373eac9e1fb49"
|
||||
|
||||
@@ -24,7 +24,7 @@ dependencies = [
|
||||
"drf-spectacular-jsonapi==0.5.1",
|
||||
"gunicorn==23.0.0",
|
||||
"lxml==5.3.2",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.16",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
|
||||
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
||||
@@ -44,7 +44,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.17.1"
|
||||
version = "1.16.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -40,6 +40,7 @@ class ApiConfig(AppConfig):
|
||||
self._ensure_crypto_keys()
|
||||
|
||||
load_prowler_compliance()
|
||||
self._initialize_attack_surface_mapping()
|
||||
|
||||
def _ensure_crypto_keys(self):
|
||||
"""
|
||||
@@ -167,3 +168,13 @@ 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)
|
||||
|
||||
@@ -43,7 +43,6 @@ from api.models import (
|
||||
ResourceTag,
|
||||
Role,
|
||||
Scan,
|
||||
ScanCategorySummary,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
@@ -158,9 +157,6 @@ 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(
|
||||
@@ -192,9 +188,6 @@ 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:
|
||||
@@ -769,7 +762,7 @@ class RoleFilter(FilterSet):
|
||||
|
||||
class ComplianceOverviewFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
scan_id = UUIDFilter(field_name="scan_id", required=True)
|
||||
scan_id = UUIDFilter(field_name="scan_id")
|
||||
region = CharFilter(field_name="region")
|
||||
|
||||
class Meta:
|
||||
@@ -1103,22 +1096,3 @@ 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 = {}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 5.1.14 on 2025-12-10
|
||||
|
||||
from django.db import migrations
|
||||
from tasks.tasks import backfill_daily_severity_summaries_task
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
def trigger_backfill_task(apps, schema_editor):
|
||||
"""
|
||||
Trigger the backfill task for all tenants.
|
||||
|
||||
This dispatches backfill_daily_severity_summaries_task for each tenant
|
||||
in the system to populate DailySeveritySummary records from historical scans.
|
||||
"""
|
||||
tenant_ids = Tenant.objects.using(MainRouter.admin_db).values_list("id", flat=True)
|
||||
|
||||
for tenant_id in tenant_ids:
|
||||
backfill_daily_severity_summaries_task.delay(tenant_id=str(tenant_id), days=90)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0061_daily_severity_summary"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(trigger_backfill_task, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,111 +0,0 @@
|
||||
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",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="api.tenant",
|
||||
),
|
||||
),
|
||||
(
|
||||
"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"),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"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",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
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"],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
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,
|
||||
help_text="Categories from check metadata for efficient filtering",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -716,19 +716,14 @@ class Resource(RowLevelSecurityProtectedModel):
|
||||
self.clear_tags()
|
||||
return
|
||||
|
||||
# Add new relationships with the tenant_id field; avoid touching the
|
||||
# Resource row unless a mapping is actually created to prevent noisy
|
||||
# updates during scans.
|
||||
mapping_created = False
|
||||
# Add new relationships with the tenant_id field
|
||||
for tag in tags:
|
||||
_, created = ResourceTagMapping.objects.update_or_create(
|
||||
ResourceTagMapping.objects.update_or_create(
|
||||
tag=tag, resource=self, tenant_id=self.tenant_id
|
||||
)
|
||||
mapping_created = mapping_created or created
|
||||
|
||||
if mapping_created:
|
||||
# Only bump updated_at when the tag set truly changed
|
||||
self.save(update_fields=["updated_at"])
|
||||
# Save the instance
|
||||
self.save()
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resources"
|
||||
@@ -873,14 +868,6 @@ 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)
|
||||
|
||||
@@ -1964,64 +1951,6 @@ 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.17.1
|
||||
version: 1.16.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -711,7 +711,6 @@ paths:
|
||||
- severity
|
||||
- check_id
|
||||
- check_metadata
|
||||
- categories
|
||||
- raw_result
|
||||
- inserted_at
|
||||
- updated_at
|
||||
@@ -724,19 +723,6 @@ 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:
|
||||
@@ -1219,7 +1205,6 @@ paths:
|
||||
- severity
|
||||
- check_id
|
||||
- check_metadata
|
||||
- categories
|
||||
- raw_result
|
||||
- inserted_at
|
||||
- updated_at
|
||||
@@ -1280,19 +1265,6 @@ 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:
|
||||
@@ -1750,7 +1722,6 @@ paths:
|
||||
- severity
|
||||
- check_id
|
||||
- check_metadata
|
||||
- categories
|
||||
- raw_result
|
||||
- inserted_at
|
||||
- updated_at
|
||||
@@ -1763,19 +1734,6 @@ 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:
|
||||
@@ -2193,23 +2151,9 @@ 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:
|
||||
@@ -2665,23 +2609,9 @@ 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:
|
||||
@@ -4569,7 +4499,7 @@ paths:
|
||||
description: No response body
|
||||
/api/v1/overviews/attack-surfaces:
|
||||
get:
|
||||
operationId: overviews_attack_surfaces_list
|
||||
operationId: overviews_attack_surfaces_retrieve
|
||||
description: Retrieve aggregated attack surface metrics from latest completed
|
||||
scans per provider.
|
||||
summary: Get attack surface overview
|
||||
@@ -4585,116 +4515,31 @@ 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_id__in]
|
||||
name: filter[provider_type.in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
type: string
|
||||
description: Filter by multiple provider types (comma-separated)
|
||||
- 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
|
||||
- muted_failed_findings
|
||||
- -muted_failed_findings
|
||||
explode: false
|
||||
description: Filter by provider type (aws, azure, gcp, etc.)
|
||||
tags:
|
||||
- Overview
|
||||
security:
|
||||
@@ -4704,164 +4549,7 @@ paths:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$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'
|
||||
$ref: '#/components/schemas/AttackSurfaceOverviewResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/findings:
|
||||
get:
|
||||
@@ -5263,7 +4951,7 @@ paths:
|
||||
summary: Get findings severity data over time
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[findings-severity-over-time]
|
||||
name: fields[findings-severity-timeseries]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
@@ -5284,12 +4972,10 @@ 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:
|
||||
@@ -11160,46 +10846,23 @@ 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
|
||||
CategoryOverview:
|
||||
AttackSurfaceOverviewResponse:
|
||||
type: object
|
||||
required:
|
||||
- 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
|
||||
data:
|
||||
$ref: '#/components/schemas/AttackSurfaceOverview'
|
||||
required:
|
||||
- data
|
||||
ComplianceOverview:
|
||||
type: object
|
||||
required:
|
||||
@@ -11416,13 +11079,6 @@ 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
|
||||
@@ -11573,15 +11229,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- services
|
||||
- regions
|
||||
- resource_types
|
||||
- categories
|
||||
FindingMetadataResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -14284,24 +13935,6 @@ 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:
|
||||
|
||||
@@ -4267,74 +4267,6 @@ 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:
|
||||
@@ -7326,6 +7258,7 @@ 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,
|
||||
@@ -7337,6 +7270,13 @@ 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,
|
||||
@@ -7362,7 +7302,11 @@ class TestOverviewViewSet:
|
||||
muted_failed=2,
|
||||
)
|
||||
|
||||
response = authenticated_client.get(reverse("overview-attack-surface"))
|
||||
with patch(
|
||||
"api.v1.views._get_attack_surface_mapping_from_provider",
|
||||
return_value=mapping,
|
||||
):
|
||||
response = authenticated_client.get(reverse("overview-attack-surface"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 4
|
||||
@@ -7370,10 +7314,19 @@ 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,
|
||||
@@ -7400,6 +7353,13 @@ 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,
|
||||
@@ -7417,15 +7377,20 @@ class TestOverviewViewSet:
|
||||
muted_failed=3,
|
||||
)
|
||||
|
||||
response = authenticated_client.get(
|
||||
reverse("overview-attack-surface"),
|
||||
{"filter[provider_id]": str(provider1.id)},
|
||||
)
|
||||
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)},
|
||||
)
|
||||
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
|
||||
@@ -7668,219 +7633,6 @@ 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:
|
||||
|
||||
@@ -382,18 +382,10 @@ 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)
|
||||
|
||||
@@ -1301,7 +1301,6 @@ class FindingSerializer(RLSSerializer):
|
||||
"severity",
|
||||
"check_id",
|
||||
"check_metadata",
|
||||
"categories",
|
||||
"raw_result",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
@@ -1357,7 +1356,6 @@ 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.")
|
||||
|
||||
@@ -2244,24 +2242,12 @@ class AttackSurfaceOverviewSerializer(BaseSerializerV1):
|
||||
total_findings = serializers.IntegerField()
|
||||
failed_findings = serializers.IntegerField()
|
||||
muted_failed_findings = serializers.IntegerField()
|
||||
|
||||
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}"
|
||||
check_ids = serializers.ListField(
|
||||
child=serializers.CharField(), allow_empty=True, default=list, read_only=True
|
||||
)
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "category-overviews"
|
||||
resource_name = "attack-surface-overviews"
|
||||
|
||||
|
||||
class OverviewRegionSerializer(serializers.Serializer):
|
||||
|
||||
@@ -74,6 +74,7 @@ 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,
|
||||
@@ -99,7 +100,6 @@ from api.db_utils import rls_transaction
|
||||
from api.exceptions import TaskFailedException
|
||||
from api.filters import (
|
||||
AttackSurfaceOverviewFilter,
|
||||
CategoryOverviewFilter,
|
||||
ComplianceOverviewFilter,
|
||||
CustomDjangoFilterBackend,
|
||||
DailySeveritySummaryFilter,
|
||||
@@ -157,7 +157,6 @@ from api.models import (
|
||||
SAMLDomainIndex,
|
||||
SAMLToken,
|
||||
Scan,
|
||||
ScanCategorySummary,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
@@ -179,7 +178,6 @@ 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,
|
||||
@@ -359,7 +357,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.17.1"
|
||||
spectacular_settings.VERSION = "1.16.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -2762,15 +2760,12 @@ 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]"
|
||||
):
|
||||
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}
|
||||
queryset = queryset.filter(scan_id__in=scans.split(","))
|
||||
scan_based_filters = {"id__in": scans.split(",")}
|
||||
else:
|
||||
exact = query_params.get("filter[inserted_at]")
|
||||
gte = query_params.get("filter[inserted_at__gte]")
|
||||
@@ -2814,7 +2809,6 @@ 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():
|
||||
@@ -2861,31 +2855,10 @@ 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)
|
||||
@@ -2990,36 +2963,10 @@ 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)
|
||||
@@ -4079,19 +4026,32 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
|
||||
summary="Get attack surface overview",
|
||||
description="Retrieve aggregated attack surface metrics from latest completed scans per provider.",
|
||||
tags=["Overview"],
|
||||
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)},
|
||||
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)",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
@@ -4140,8 +4100,6 @@ 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):
|
||||
@@ -4153,10 +4111,6 @@ 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):
|
||||
@@ -4242,41 +4196,29 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
filterset = filterset_class(normalized_params, queryset=queryset)
|
||||
return filterset.qs
|
||||
|
||||
def _latest_scan_ids_for_allowed_providers(self, tenant_id, provider_filters=None):
|
||||
def _latest_scan_ids_for_allowed_providers(self, tenant_id):
|
||||
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 (
|
||||
queryset.order_by("provider_id", "-inserted_at")
|
||||
Scan.all_objects.filter(
|
||||
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
|
||||
)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="providers")
|
||||
def providers(self, request):
|
||||
@@ -4883,13 +4825,22 @@ 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),
|
||||
@@ -4912,71 +4863,12 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
}
|
||||
|
||||
response_data = [
|
||||
{"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,
|
||||
},
|
||||
{
|
||||
"attack_surface_type": key,
|
||||
**value,
|
||||
"check_ids": attack_surface_check_ids.get(key, []),
|
||||
}
|
||||
)
|
||||
|
||||
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())
|
||||
for key, value in results.items()
|
||||
]
|
||||
|
||||
return Response(
|
||||
|
||||
@@ -19,6 +19,8 @@ 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
|
||||
|
||||
@@ -11,10 +11,7 @@ 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,
|
||||
backfill_scan_category_summaries,
|
||||
)
|
||||
from tasks.jobs.backfill import backfill_resource_scan_summaries
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import (
|
||||
@@ -39,7 +36,6 @@ from api.models import (
|
||||
SAMLConfiguration,
|
||||
SAMLDomainIndex,
|
||||
Scan,
|
||||
ScanCategorySummary,
|
||||
ScanSummary,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
@@ -1275,113 +1271,6 @@ 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]
|
||||
@@ -1596,30 +1485,6 @@ 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}"}
|
||||
|
||||
|
||||
@@ -61,5 +61,4 @@ 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
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from collections import defaultdict
|
||||
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
|
||||
@@ -11,12 +8,10 @@ from api.models import (
|
||||
ComplianceOverviewSummary,
|
||||
ComplianceRequirementOverview,
|
||||
DailySeveritySummary,
|
||||
Finding,
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
ResourceScanSummary,
|
||||
Scan,
|
||||
ScanCategorySummary,
|
||||
ScanSummary,
|
||||
StateChoices,
|
||||
)
|
||||
@@ -191,6 +186,10 @@ def backfill_daily_severity_summaries(tenant_id: str, days: int = None):
|
||||
Backfill DailySeveritySummary from completed scans.
|
||||
Groups by provider+date, keeps latest scan per day.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
@@ -277,67 +276,3 @@ 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,41 +11,6 @@ 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:
|
||||
"""
|
||||
@@ -318,41 +283,20 @@ 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. Only includes
|
||||
models that support text input, text output or tool calling.
|
||||
as the API doesn't provide separate display names.
|
||||
|
||||
Raises:
|
||||
Exception: If the API call fails.
|
||||
"""
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
models = client.models.list()
|
||||
|
||||
# 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
|
||||
# OpenAI uses model.id for both ID and display name
|
||||
return {m.id: m.id for m in getattr(models, "data", [])}
|
||||
|
||||
|
||||
def _fetch_openai_compatible_models(base_url: str, api_key: str) -> Dict[str, str]:
|
||||
|
||||
@@ -243,28 +243,15 @@ def _safe_getattr(obj, attr: str, default: str = "N/A") -> str:
|
||||
|
||||
|
||||
def _create_info_table_style() -> TableStyle:
|
||||
"""Create a reusable table style for information/metadata tables.
|
||||
|
||||
ReportLab TableStyle coordinate system:
|
||||
- Format: (COMMAND, (start_col, start_row), (end_col, end_row), value)
|
||||
- Coordinates use (column, row) format, starting at (0, 0) for top-left cell
|
||||
- Negative indices work like Python slicing: -1 means "last row/column"
|
||||
- (0, 0) to (0, -1) = entire first column (all rows)
|
||||
- (0, 0) to (-1, 0) = entire first row (all columns)
|
||||
- (0, 0) to (-1, -1) = entire table
|
||||
- Styles are applied in order; later rules override earlier ones
|
||||
"""
|
||||
"""Create a reusable table style for information/metadata tables."""
|
||||
return TableStyle(
|
||||
[
|
||||
# Column 0 (labels): blue background with white text
|
||||
("BACKGROUND", (0, 0), (0, -1), COLOR_BLUE),
|
||||
("TEXTCOLOR", (0, 0), (0, -1), COLOR_WHITE),
|
||||
("FONTNAME", (0, 0), (0, -1), "FiraCode"),
|
||||
# Column 1 (values): light blue background with gray text
|
||||
("BACKGROUND", (1, 0), (1, -1), COLOR_BG_BLUE),
|
||||
("TEXTCOLOR", (1, 0), (1, -1), COLOR_GRAY),
|
||||
("FONTNAME", (1, 0), (1, -1), "PlusJakartaSans"),
|
||||
# Apply to entire table
|
||||
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 11),
|
||||
@@ -278,30 +265,19 @@ def _create_info_table_style() -> TableStyle:
|
||||
|
||||
|
||||
def _create_header_table_style(header_color: colors.Color = None) -> TableStyle:
|
||||
"""Create a reusable table style for tables with headers.
|
||||
|
||||
ReportLab TableStyle coordinate system:
|
||||
- Format: (COMMAND, (start_col, start_row), (end_col, end_row), value)
|
||||
- (0, 0) to (-1, 0) = entire first row (header row)
|
||||
- (1, 1) to (-1, -1) = all data cells (excludes header row and first column)
|
||||
- See _create_info_table_style() for full coordinate system documentation
|
||||
"""
|
||||
"""Create a reusable table style for tables with headers."""
|
||||
if header_color is None:
|
||||
header_color = COLOR_BLUE
|
||||
|
||||
return TableStyle(
|
||||
[
|
||||
# Header row (row 0): colored background with white text
|
||||
("BACKGROUND", (0, 0), (-1, 0), header_color),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE),
|
||||
("FONTNAME", (0, 0), (-1, 0), "FiraCode"),
|
||||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||||
# Apply to entire table
|
||||
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
# Data cells (excluding header): smaller font
|
||||
("FONTSIZE", (1, 1), (-1, -1), 9),
|
||||
# Apply to entire table
|
||||
("GRID", (0, 0), (-1, -1), 1, COLOR_GRID_GRAY),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), PADDING_MEDIUM),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), PADDING_MEDIUM),
|
||||
@@ -312,30 +288,18 @@ def _create_header_table_style(header_color: colors.Color = None) -> TableStyle:
|
||||
|
||||
|
||||
def _create_findings_table_style() -> TableStyle:
|
||||
"""Create a reusable table style for findings tables.
|
||||
|
||||
ReportLab TableStyle coordinate system:
|
||||
- Format: (COMMAND, (start_col, start_row), (end_col, end_row), value)
|
||||
- (0, 0) to (-1, 0) = entire first row (header row)
|
||||
- (0, 0) to (0, 0) = only the top-left cell
|
||||
- See _create_info_table_style() for full coordinate system documentation
|
||||
"""
|
||||
"""Create a reusable table style for findings tables."""
|
||||
return TableStyle(
|
||||
[
|
||||
# Header row (row 0): colored background with white text
|
||||
("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE),
|
||||
("FONTNAME", (0, 0), (-1, 0), "FiraCode"),
|
||||
# Only top-left cell centered (for index/number column)
|
||||
("ALIGN", (0, 0), (0, 0), "CENTER"),
|
||||
# Apply to entire table
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 9),
|
||||
("GRID", (0, 0), (-1, -1), 0.1, COLOR_BORDER_GRAY),
|
||||
# Remove padding only from top-left cell
|
||||
("LEFTPADDING", (0, 0), (0, 0), 0),
|
||||
("RIGHTPADDING", (0, 0), (0, 0), 0),
|
||||
# Apply to entire table
|
||||
("TOPPADDING", (0, 0), (-1, -1), PADDING_SMALL),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), PADDING_SMALL),
|
||||
]
|
||||
@@ -1139,15 +1103,11 @@ def generate_threatscore_report(
|
||||
elements.append(Spacer(1, 0.5 * inch))
|
||||
|
||||
# Add compliance information table
|
||||
provider_alias = provider_obj.alias or "N/A"
|
||||
info_data = [
|
||||
["Framework:", compliance_framework],
|
||||
["ID:", compliance_id],
|
||||
["Name:", Paragraph(compliance_name, normal_center)],
|
||||
["Version:", compliance_version],
|
||||
["Provider:", provider_type.upper()],
|
||||
["Account ID:", provider_obj.uid],
|
||||
["Alias:", provider_alias],
|
||||
["Scan ID:", scan_id],
|
||||
["Description:", Paragraph(compliance_description, normal_center)],
|
||||
]
|
||||
@@ -2099,15 +2059,12 @@ def generate_ens_report(
|
||||
elements.append(Spacer(1, 0.5 * inch))
|
||||
|
||||
# Add compliance information table
|
||||
provider_alias = provider_obj.alias or "N/A"
|
||||
info_data = [
|
||||
["Framework:", compliance_framework],
|
||||
["ID:", compliance_id],
|
||||
["Nombre:", Paragraph(compliance_name, normal_center)],
|
||||
["Versión:", compliance_version],
|
||||
["Proveedor:", provider_type.upper()],
|
||||
["Account ID:", provider_obj.uid],
|
||||
["Alias:", provider_alias],
|
||||
["Scan ID:", scan_id],
|
||||
["Descripción:", Paragraph(compliance_description, normal_center)],
|
||||
]
|
||||
@@ -2115,12 +2072,12 @@ def generate_ens_report(
|
||||
info_table.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (0, -1), colors.Color(0.2, 0.4, 0.6)),
|
||||
("TEXTCOLOR", (0, 0), (0, -1), colors.white),
|
||||
("FONTNAME", (0, 0), (0, -1), "FiraCode"),
|
||||
("BACKGROUND", (1, 0), (1, -1), colors.Color(0.95, 0.97, 1.0)),
|
||||
("TEXTCOLOR", (1, 0), (1, -1), colors.Color(0.2, 0.2, 0.2)),
|
||||
("FONTNAME", (1, 0), (1, -1), "PlusJakartaSans"),
|
||||
("BACKGROUND", (0, 0), (0, 6), colors.Color(0.2, 0.4, 0.6)),
|
||||
("TEXTCOLOR", (0, 0), (0, 6), colors.white),
|
||||
("FONTNAME", (0, 0), (0, 6), "FiraCode"),
|
||||
("BACKGROUND", (1, 0), (1, 6), colors.Color(0.95, 0.97, 1.0)),
|
||||
("TEXTCOLOR", (1, 0), (1, 6), colors.Color(0.2, 0.2, 0.2)),
|
||||
("FONTNAME", (1, 0), (1, 6), "PlusJakartaSans"),
|
||||
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 11),
|
||||
@@ -3040,14 +2997,11 @@ def generate_nis2_report(
|
||||
elements.append(Spacer(1, 0.3 * inch))
|
||||
|
||||
# Compliance metadata table
|
||||
provider_alias = provider_obj.alias or "N/A"
|
||||
metadata_data = [
|
||||
["Framework:", compliance_framework],
|
||||
["Name:", Paragraph(compliance_name, normal_center)],
|
||||
["Version:", compliance_version or "N/A"],
|
||||
["Provider:", provider_type.upper()],
|
||||
["Account ID:", provider_obj.uid],
|
||||
["Alias:", provider_alias],
|
||||
["Scan ID:", scan_id],
|
||||
["Description:", Paragraph(compliance_description, normal_center)],
|
||||
]
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -40,7 +39,6 @@ from api.models import (
|
||||
ResourceScanSummary,
|
||||
ResourceTag,
|
||||
Scan,
|
||||
ScanCategorySummary,
|
||||
ScanSummary,
|
||||
StateChoices,
|
||||
)
|
||||
@@ -79,6 +77,7 @@ 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
|
||||
@@ -89,40 +88,6 @@ 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
|
||||
|
||||
@@ -433,7 +398,6 @@ 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.
|
||||
@@ -454,7 +418,6 @@ 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 = []
|
||||
@@ -610,12 +573,11 @@ 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=check_metadata,
|
||||
check_metadata=finding.get_metadata(),
|
||||
status=status,
|
||||
status_extended=finding.status_extended,
|
||||
severity=finding.severity,
|
||||
@@ -628,7 +590,6 @@ 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))
|
||||
@@ -643,16 +604,6 @@ 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
|
||||
@@ -752,7 +703,6 @@ 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
|
||||
|
||||
@@ -842,7 +792,6 @@ 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
|
||||
@@ -902,33 +851,13 @@ 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
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ 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,
|
||||
@@ -535,21 +534,6 @@ 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,19 +4,14 @@ 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")
|
||||
@@ -51,45 +46,6 @@ 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):
|
||||
@@ -216,47 +172,3 @@ 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
|
||||
|
||||
@@ -28,7 +28,6 @@ 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}"
|
||||
|
||||
@@ -20,7 +20,6 @@ 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,
|
||||
@@ -1378,7 +1377,6 @@ 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),
|
||||
@@ -1396,7 +1394,6 @@ class TestProcessFindingMicroBatch:
|
||||
unique_resources,
|
||||
scan_resource_cache,
|
||||
mute_rules_cache,
|
||||
scan_categories_cache,
|
||||
)
|
||||
|
||||
created_finding = Finding.objects.get(uid=finding.uid)
|
||||
@@ -1489,7 +1486,6 @@ 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),
|
||||
@@ -1507,7 +1503,6 @@ class TestProcessFindingMicroBatch:
|
||||
unique_resources,
|
||||
scan_resource_cache,
|
||||
mute_rules_cache,
|
||||
scan_categories_cache,
|
||||
)
|
||||
|
||||
existing_resource.refresh_from_db()
|
||||
@@ -1615,7 +1610,6 @@ 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),
|
||||
@@ -1634,7 +1628,6 @@ class TestProcessFindingMicroBatch:
|
||||
unique_resources,
|
||||
scan_resource_cache,
|
||||
mute_rules_cache,
|
||||
scan_categories_cache,
|
||||
)
|
||||
|
||||
# Verify the long UID finding was NOT created
|
||||
@@ -1655,118 +1648,6 @@ 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:
|
||||
@@ -3875,150 +3756,3 @@ 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}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_threatscore
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_threatscore(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_ID",
|
||||
)
|
||||
@@ -407,11 +407,9 @@ def display_data(
|
||||
compliance_module = importlib.import_module(
|
||||
f"dashboard.compliance.{current}"
|
||||
)
|
||||
# Build subset list based on available columns
|
||||
dedup_columns = ["CHECKID", "STATUS", "RESOURCEID", "STATUSEXTENDED"]
|
||||
if "MUTED" in data.columns:
|
||||
dedup_columns.insert(2, "MUTED")
|
||||
data = data.drop_duplicates(subset=dedup_columns)
|
||||
data = data.drop_duplicates(
|
||||
subset=["CHECKID", "STATUS", "MUTED", "RESOURCEID", "STATUSEXTENDED"]
|
||||
)
|
||||
|
||||
if "threatscore" in analytics_input:
|
||||
data = get_threatscore_mean_by_pillar(data)
|
||||
@@ -654,7 +652,6 @@ def get_table(current_compliance, table):
|
||||
def get_threatscore_mean_by_pillar(df):
|
||||
score_per_pillar = {}
|
||||
max_score_per_pillar = {}
|
||||
counted_findings_per_pillar = {}
|
||||
|
||||
for _, row in df.iterrows():
|
||||
pillar = (
|
||||
@@ -666,18 +663,6 @@ def get_threatscore_mean_by_pillar(df):
|
||||
if pillar not in score_per_pillar:
|
||||
score_per_pillar[pillar] = 0
|
||||
max_score_per_pillar[pillar] = 0
|
||||
counted_findings_per_pillar[pillar] = set()
|
||||
|
||||
# Skip muted findings for score calculation
|
||||
is_muted = "MUTED" in df.columns and row.get("MUTED") == "True"
|
||||
if is_muted:
|
||||
continue
|
||||
|
||||
# Create unique finding identifier to avoid counting duplicates
|
||||
finding_id = f"{row.get('CHECKID', '')}_{row.get('RESOURCEID', '')}"
|
||||
if finding_id in counted_findings_per_pillar[pillar]:
|
||||
continue
|
||||
counted_findings_per_pillar[pillar].add(finding_id)
|
||||
|
||||
level_of_risk = pd.to_numeric(
|
||||
row["REQUIREMENTS_ATTRIBUTES_LEVELOFRISK"], errors="coerce"
|
||||
@@ -721,10 +706,6 @@ def get_table_prowler_threatscore(df):
|
||||
score_per_pillar = {}
|
||||
max_score_per_pillar = {}
|
||||
pillars = {}
|
||||
counted_findings_per_pillar = {}
|
||||
counted_pass = set()
|
||||
counted_fail = set()
|
||||
counted_muted = set()
|
||||
|
||||
df_copy = df.copy()
|
||||
|
||||
@@ -739,24 +720,6 @@ def get_table_prowler_threatscore(df):
|
||||
pillars[pillar] = {"FAIL": 0, "PASS": 0, "MUTED": 0}
|
||||
score_per_pillar[pillar] = 0
|
||||
max_score_per_pillar[pillar] = 0
|
||||
counted_findings_per_pillar[pillar] = set()
|
||||
|
||||
# Create unique finding identifier
|
||||
finding_id = f"{row.get('CHECKID', '')}_{row.get('RESOURCEID', '')}"
|
||||
|
||||
# Check if muted
|
||||
is_muted = "MUTED" in df_copy.columns and row.get("MUTED") == "True"
|
||||
|
||||
# Count muted findings (separate from score calculation)
|
||||
if is_muted and finding_id not in counted_muted:
|
||||
counted_muted.add(finding_id)
|
||||
pillars[pillar]["MUTED"] += 1
|
||||
continue # Skip muted findings for score calculation
|
||||
|
||||
# Skip if already counted for this pillar
|
||||
if finding_id in counted_findings_per_pillar[pillar]:
|
||||
continue
|
||||
counted_findings_per_pillar[pillar].add(finding_id)
|
||||
|
||||
level_of_risk = pd.to_numeric(
|
||||
row["REQUIREMENTS_ATTRIBUTES_LEVELOFRISK"], errors="coerce"
|
||||
@@ -775,14 +738,13 @@ def get_table_prowler_threatscore(df):
|
||||
max_score_per_pillar[pillar] += level_of_risk * weight
|
||||
|
||||
if row["STATUS"] == "PASS":
|
||||
if finding_id not in counted_pass:
|
||||
counted_pass.add(finding_id)
|
||||
pillars[pillar]["PASS"] += 1
|
||||
pillars[pillar]["PASS"] += 1
|
||||
score_per_pillar[pillar] += level_of_risk * weight
|
||||
elif row["STATUS"] == "FAIL":
|
||||
if finding_id not in counted_fail:
|
||||
counted_fail.add(finding_id)
|
||||
pillars[pillar]["FAIL"] += 1
|
||||
pillars[pillar]["FAIL"] += 1
|
||||
|
||||
if "MUTED" in row and row["MUTED"] == "True":
|
||||
pillars[pillar]["MUTED"] += 1
|
||||
|
||||
result_df = []
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@ services:
|
||||
volumes:
|
||||
- "./ui:/app"
|
||||
- "/app/node_modules"
|
||||
depends_on:
|
||||
mcp-server:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine3.20
|
||||
@@ -60,11 +57,7 @@ services:
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER} -d ${POSTGRES_DB}'",
|
||||
]
|
||||
test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER} -d ${POSTGRES_DB}'"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -125,32 +118,6 @@ services:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "beat"
|
||||
|
||||
mcp-server:
|
||||
build:
|
||||
context: ./mcp_server
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- PROWLER_MCP_TRANSPORT_MODE=http
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./mcp_server/prowler_mcp_server:/app/prowler_mcp_server
|
||||
- ./mcp_server/pyproject.toml:/app/pyproject.toml
|
||||
- ./mcp_server/entrypoint.sh:/app/entrypoint.sh
|
||||
command: ["uvicorn", "--host", "0.0.0.0", "--port", "8000"]
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget -q -O /dev/null http://127.0.0.1:8000/health || exit 1",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
outputs:
|
||||
driver: local
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
# Production Docker Compose configuration
|
||||
# Uses pre-built images from Docker Hub (prowlercloud/*)
|
||||
#
|
||||
# For development with local builds and hot-reload, use docker-compose-dev.yml instead:
|
||||
# docker compose -f docker-compose-dev.yml up
|
||||
#
|
||||
services:
|
||||
api:
|
||||
hostname: "prowler-api"
|
||||
@@ -32,9 +26,6 @@ services:
|
||||
required: false
|
||||
ports:
|
||||
- ${UI_PORT:-3000}:${UI_PORT:-3000}
|
||||
depends_on:
|
||||
mcp-server:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine3.20
|
||||
@@ -102,22 +93,6 @@ services:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "beat"
|
||||
|
||||
mcp-server:
|
||||
image: prowlercloud/prowler-mcp:${PROWLER_MCP_VERSION:-stable}
|
||||
environment:
|
||||
- PROWLER_MCP_TRANSPORT_MODE=http
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
ports:
|
||||
- "8000:8000"
|
||||
command: ["uvicorn", "--host", "0.0.0.0", "--port", "8000"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8000/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
output:
|
||||
driver: local
|
||||
|
||||
@@ -213,5 +213,3 @@ 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 |
|
||||
@@ -1,447 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -63,82 +63,6 @@ Other Commands for Running Tests
|
||||
Refer to the [pytest documentation](https://docs.pytest.org/en/7.1.x/getting-started.html) for more details.
|
||||
|
||||
</Note>
|
||||
|
||||
## AWS Service Dependency Table (CI Optimization)
|
||||
|
||||
To optimize CI pipeline execution time, the GitHub Actions workflow for AWS tests uses a **service dependency table** that determines which tests to run based on changed files. This ensures that when a service is modified, all dependent services are also tested.
|
||||
|
||||
### How It Works
|
||||
|
||||
The dependency table is defined in `.github/workflows/sdk-tests.yml` within the "Resolve AWS services under test" step. When files in a specific AWS service are changed:
|
||||
|
||||
1. Tests for the changed service are run
|
||||
2. Tests for all services that **depend on** the changed service are also run
|
||||
|
||||
For example, if you modify the `ec2` service, tests will also run for `dlm`, `dms`, `elbv2`, `emr`, `inspector2`, `rds`, `redshift`, `route53`, `shield`, `ssm`, and `workspaces` because these services use the EC2 client.
|
||||
|
||||
### Current Dependency Table
|
||||
|
||||
The table maps a service (key) to the list of services that depend on it (values):
|
||||
|
||||
| Service | Dependent Services |
|
||||
|---------|-------------------|
|
||||
| `acm` | `elb` |
|
||||
| `autoscaling` | `dynamodb` |
|
||||
| `awslambda` | `ec2`, `inspector2` |
|
||||
| `backup` | `dynamodb`, `ec2`, `rds` |
|
||||
| `cloudfront` | `shield` |
|
||||
| `cloudtrail` | `awslambda`, `cloudwatch` |
|
||||
| `cloudwatch` | `bedrock` |
|
||||
| `ec2` | `dlm`, `dms`, `elbv2`, `emr`, `inspector2`, `rds`, `redshift`, `route53`, `shield`, `ssm` |
|
||||
| `ecr` | `inspector2` |
|
||||
| `elb` | `shield` |
|
||||
| `elbv2` | `shield` |
|
||||
| `globalaccelerator` | `shield` |
|
||||
| `iam` | `bedrock`, `cloudtrail`, `cloudwatch`, `codebuild` |
|
||||
| `kafka` | `firehose` |
|
||||
| `kinesis` | `firehose` |
|
||||
| `kms` | `kafka` |
|
||||
| `organizations` | `iam`, `servicecatalog` |
|
||||
| `route53` | `shield` |
|
||||
| `s3` | `bedrock`, `cloudfront`, `cloudtrail`, `macie` |
|
||||
| `ssm` | `ec2` |
|
||||
| `vpc` | `awslambda`, `ec2`, `efs`, `elasticache`, `neptune`, `networkfirewall`, `rds`, `redshift`, `workspaces` |
|
||||
| `waf` | `elbv2` |
|
||||
| `wafv2` | `cognito`, `elbv2` |
|
||||
|
||||
### When to Update the Table
|
||||
|
||||
You must update the dependency table when:
|
||||
|
||||
1. **A new check or service uses another service's client**: If your check imports a client from another service (e.g., `from prowler.providers.aws.services.ec2.ec2_client import ec2_client` in a non-ec2 check), add your service to the dependent services list of that client's service.
|
||||
|
||||
2. **A service relationship changes**: If you remove or add a service client dependency in an existing check, update the table accordingly.
|
||||
|
||||
### How to Update the Table
|
||||
|
||||
1. Open `.github/workflows/sdk-tests.yml`
|
||||
2. Find the `dependents` dictionary in the "Resolve AWS services under test" step
|
||||
3. Add or modify entries as needed
|
||||
4. **Update this documentation page** (`docs/developer-guide/unit-testing.mdx`) to reflect the changes in the [Current Dependency Table](#current-dependency-table) section above
|
||||
|
||||
```python
|
||||
dependents = {
|
||||
# ... existing entries ...
|
||||
"service_being_used": ["service_that_uses_it"],
|
||||
}
|
||||
```
|
||||
|
||||
**Example**: If you create a new check in the `newservice` service that imports `ec2_client`, add `newservice` to the `ec2` entry:
|
||||
|
||||
```python
|
||||
"ec2": ["dlm", "dms", "elbv2", "emr", "inspector2", "newservice", "rds", "redshift", "route53", "shield", "ssm"],
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Failing to update this table when adding cross-service dependencies may result in CI tests passing even when related functionality is broken, as the dependent service tests won't be triggered.
|
||||
</Warning>
|
||||
|
||||
## AWS Testing Approaches
|
||||
|
||||
For AWS provider, different testing approaches apply based on API coverage based on several criteria.
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Welcome",
|
||||
"pages": ["introduction"]
|
||||
"pages": [
|
||||
"introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Prowler Cloud",
|
||||
@@ -49,7 +51,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Prowler Lighthouse AI",
|
||||
"pages": ["getting-started/products/prowler-lighthouse-ai"]
|
||||
"pages": [
|
||||
"getting-started/products/prowler-lighthouse-ai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Prowler MCP Server",
|
||||
@@ -149,7 +153,9 @@
|
||||
"user-guide/cli/tutorials/quick-inventory",
|
||||
{
|
||||
"group": "Tutorials",
|
||||
"pages": ["user-guide/cli/tutorials/parallel-execution"]
|
||||
"pages": [
|
||||
"user-guide/cli/tutorials/parallel-execution"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -237,7 +243,9 @@
|
||||
},
|
||||
{
|
||||
"group": "LLM",
|
||||
"pages": ["user-guide/providers/llm/getting-started-llm"]
|
||||
"pages": [
|
||||
"user-guide/providers/llm/getting-started-llm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Oracle Cloud Infrastructure",
|
||||
@@ -250,7 +258,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Compliance",
|
||||
"pages": ["user-guide/compliance/tutorials/threatscore"]
|
||||
"pages": [
|
||||
"user-guide/compliance/tutorials/threatscore"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -267,8 +277,7 @@
|
||||
"developer-guide/outputs",
|
||||
"developer-guide/integrations",
|
||||
"developer-guide/security-compliance-framework",
|
||||
"developer-guide/lighthouse",
|
||||
"developer-guide/mcp-server"
|
||||
"developer-guide/lighthouse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -304,15 +313,21 @@
|
||||
},
|
||||
{
|
||||
"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 | 24 tools | Yes |
|
||||
| Prowler Cloud/App | 28 tools | Yes |
|
||||
|
||||
## Tool Naming Convention
|
||||
|
||||
@@ -20,6 +20,39 @@ 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 Hub Tools
|
||||
|
||||
Access Prowler's security check catalog and compliance frameworks. **No authentication required.**
|
||||
|
||||
### Check Discovery
|
||||
|
||||
- **`prowler_hub_get_checks`** - List security checks with advanced filtering options
|
||||
- **`prowler_hub_get_check_filters`** - Return available filter values for checks (providers, services, severities, categories, compliances)
|
||||
- **`prowler_hub_search_checks`** - Full-text search across check metadata
|
||||
- **`prowler_hub_get_check_raw_metadata`** - Fetch raw check metadata in JSON format
|
||||
|
||||
### Check Code
|
||||
|
||||
- **`prowler_hub_get_check_code`** - Fetch the Python implementation code for a security check
|
||||
- **`prowler_hub_get_check_fixer`** - Fetch the automated fixer code for a check (if available)
|
||||
|
||||
### Compliance Frameworks
|
||||
|
||||
- **`prowler_hub_get_compliance_frameworks`** - List and filter compliance frameworks
|
||||
- **`prowler_hub_search_compliance_frameworks`** - Full-text search across compliance frameworks
|
||||
|
||||
### Provider Information
|
||||
|
||||
- **`prowler_hub_list_providers`** - List Prowler official providers and their services
|
||||
- **`prowler_hub_get_artifacts_count`** - Get total count of checks and frameworks in Prowler Hub
|
||||
|
||||
## Prowler Documentation Tools
|
||||
|
||||
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.**
|
||||
@@ -30,97 +63,49 @@ These tools require a valid API key. See the [Configuration Guide](/getting-star
|
||||
|
||||
### 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
|
||||
- **`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
|
||||
|
||||
Tools for managing cloud provider connections in Prowler.
|
||||
- **`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
|
||||
|
||||
- **`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
|
||||
### 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
|
||||
|
||||
Tools for managing and monitoring security scans.
|
||||
- **`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
|
||||
|
||||
- **`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
|
||||
### Schedule Management
|
||||
|
||||
### Resources Management
|
||||
- **`prowler_app_schedules_daily_scan`** - Create a daily scheduled scan for a provider
|
||||
|
||||
Tools for searching, viewing, and analyzing cloud resources discovered by Prowler.
|
||||
### Processor Management
|
||||
|
||||
- **`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
|
||||
|
||||
### Compliance Management
|
||||
|
||||
Tools for viewing compliance status and framework details across all cloud providers.
|
||||
|
||||
- **`prowler_app_get_compliance_overview`** - Get high-level compliance status across all frameworks for a specific scan or provider, including pass/fail statistics per framework
|
||||
- **`prowler_app_get_compliance_framework_state_details`** - Get detailed requirement-level breakdown for a specific compliance framework, including failed requirements and associated finding IDs
|
||||
|
||||
## Prowler Hub Tools
|
||||
|
||||
Access Prowler's security check catalog and compliance frameworks. **No authentication required.**
|
||||
|
||||
Tools follow a **two-tier pattern**: lightweight listing for browsing + detailed retrieval for complete information.
|
||||
|
||||
### Check Discovery and Details
|
||||
|
||||
- **`prowler_hub_list_checks`** - List security checks with lightweight data (id, title, severity, provider) and advanced filtering options
|
||||
- **`prowler_hub_semantic_search_checks`** - Full-text search across check metadata with lightweight results
|
||||
- **`prowler_hub_get_check_details`** - Get comprehensive details for a specific check including risk, remediation guidance, and compliance mappings
|
||||
|
||||
### Check Code
|
||||
|
||||
- **`prowler_hub_get_check_code`** - Fetch the Python implementation code for a security check
|
||||
- **`prowler_hub_get_check_fixer`** - Fetch the automated fixer code for a check (if available)
|
||||
|
||||
### Compliance Frameworks
|
||||
|
||||
- **`prowler_hub_list_compliances`** - List compliance frameworks with lightweight data (id, name, provider) and filtering options
|
||||
- **`prowler_hub_semantic_search_compliances`** - Full-text search across compliance frameworks with lightweight results
|
||||
- **`prowler_hub_get_compliance_details`** - Get comprehensive compliance details including requirements and mapped checks
|
||||
|
||||
### Providers Information
|
||||
|
||||
- **`prowler_hub_list_providers`** - List Prowler official providers
|
||||
- **`prowler_hub_get_provider_services`** - Get available services for a specific provider
|
||||
|
||||
## Prowler Documentation Tools
|
||||
|
||||
Search and access official Prowler documentation. **No authentication required.**
|
||||
|
||||
- **`prowler_docs_search`** - Search the official Prowler documentation using full-text search with the `term` parameter
|
||||
- **`prowler_docs_get_document`** - Retrieve the full markdown content of a specific documentation file using the path from search results
|
||||
- **`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
|
||||
|
||||
|
||||
@@ -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>",
|
||||
"API_BASE_URL": "https://api.prowler.com/api/v1"
|
||||
"PROWLER_API_BASE_URL": "https://api.prowler.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 `API_BASE_URL` is optional and defaults to Prowler Cloud API.
|
||||
Replace `/absolute/path/to/prowler/mcp_server/` with the actual path. The `PROWLER_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",
|
||||
"API_BASE_URL=https://api.prowler.com/api/v1",
|
||||
"PROWLER_API_BASE_URL=https://api.prowler.com",
|
||||
"prowlercloud/prowler-mcp"
|
||||
]
|
||||
}
|
||||
@@ -176,7 +176,7 @@ STDIO mode is only available when running the MCP server locally.
|
||||
```
|
||||
|
||||
<Note>
|
||||
The `API_BASE_URL` is optional and defaults to Prowler Cloud API.
|
||||
The `PROWLER_API_BASE_URL` is optional and defaults to Prowler Cloud API.
|
||||
</Note>
|
||||
|
||||
</Tab>
|
||||
|
||||
@@ -115,15 +115,10 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.16.0"
|
||||
PROWLER_API_VERSION="5.16.0"
|
||||
PROWLER_UI_VERSION="5.9.0"
|
||||
PROWLER_API_VERSION="5.9.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
You can find the latest versions of Prowler App in the [Releases Github section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
|
||||
</Note>
|
||||
|
||||
|
||||
#### Option 2: Using Docker Compose Pull
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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 API_BASE_URL="https://api.prowler.com/api/v1" \
|
||||
-e PROWLER_API_BASE_URL="https://api.prowler.com" \
|
||||
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 | - |
|
||||
| `API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com/api/v1` |
|
||||
| `PROWLER_API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com` |
|
||||
| `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 API_BASE_URL="https://api.prowler.com/api/v1"
|
||||
export PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
export PROWLER_MCP_TRANSPORT_MODE="http"
|
||||
```
|
||||
|
||||
```bash Windows PowerShell
|
||||
$env:PROWLER_APP_API_KEY="pk_your_api_key_here"
|
||||
$env:API_BASE_URL="https://api.prowler.com/api/v1"
|
||||
$env:PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
$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
|
||||
API_BASE_URL=https://api.prowler.com/api/v1
|
||||
PROWLER_API_BASE_URL=https://api.prowler.com
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
<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 you’re customizing policies, managing compliance, or enhancing visibility, Prowler Hub is built to support your security operations.
|
||||
Whether you’re customizing policies, managing compliance, or enhancing visibility, Prowler Hub is built to support your security operations.
|
||||
@@ -19,11 +19,12 @@ 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:
|
||||
- **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
|
||||
- **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.
|
||||
|
||||
### 2. Prowler Hub
|
||||
|
||||
@@ -48,10 +49,7 @@ 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
|
||||
- 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, and Prowler Documentation for guidance and reference.
|
||||
|
||||
## Use Cases
|
||||
|
||||
@@ -59,12 +57,12 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
|
||||
|
||||
**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"
|
||||
- "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"
|
||||
|
||||
**Security Research**
|
||||
- "Explain what the S3 bucket public access Prowler check does"
|
||||
- "Find all Prowler checks related to encryption at rest"
|
||||
- "Explain what the S3 bucket public access check does"
|
||||
- "Find all checks related to encryption at rest"
|
||||
- "What is the latest version of the CIS that Prowler is covering per provider?"
|
||||
|
||||
**Documentation & Learning**
|
||||
|
||||
|
Before Width: | Height: | Size: 743 KiB After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 872 KiB |
|
After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -24,80 +24,6 @@ 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,26 +5,18 @@ 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 IaC Formats
|
||||
## Supported Scanners
|
||||
|
||||
Prowler IaC provider scans the following Infrastructure as Code configurations for misconfigurations and secrets:
|
||||
The IaC provider leverages [Trivy](https://trivy.dev/latest/docs/scanner/vulnerability/) to support multiple scanners, including:
|
||||
|
||||
| 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 |
|
||||
- Vulnerability
|
||||
- Misconfiguration
|
||||
- Secret
|
||||
- License
|
||||
|
||||
## How It Works
|
||||
|
||||
- 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.
|
||||
- The IaC provider scans 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.
|
||||
@@ -35,10 +27,6 @@ Prowler IaC provider scans the following Infrastructure as Code configurations f
|
||||
|
||||
<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)
|
||||
@@ -75,17 +63,6 @@ Scanner selection is not configurable in Prowler App. Default scanners, misconfi
|
||||
|
||||
<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.
|
||||
@@ -126,7 +103,7 @@ Authentication for private repositories can be provided using one of the followi
|
||||
|
||||
#### Specify Scanners
|
||||
|
||||
To run only specific scanners, use the `--scanners` flag. For example, to scan only for vulnerabilities and misconfigurations:
|
||||
Scan only vulnerability and misconfiguration scanners:
|
||||
|
||||
```sh
|
||||
prowler iac --scan-path ./my-iac-directory --scanners vuln misconfig
|
||||
|
||||
@@ -47,43 +47,8 @@ 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. 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>
|
||||
- **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`.
|
||||
|
||||
3. Update your `kubeconfig` to use the ServiceAccount token:
|
||||
|
||||
|
||||
@@ -2,83 +2,18 @@
|
||||
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**.
|
||||

|
||||
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**.
|
||||

|
||||
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.
|
||||

|
||||
|
||||
### 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.
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
<VersionBadge version="5.12.0" />
|
||||
### Authentication Methods
|
||||
|
||||
You can also run MongoDB Atlas assessments directly from the CLI. Both command-line flags and environment variables are supported.
|
||||
#### Command-Line Arguments
|
||||
|
||||
### 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>
|
||||
@@ -86,28 +21,33 @@ export ATLAS_PRIVATE_KEY=<private_key>
|
||||
prowler mongodbatlas
|
||||
```
|
||||
|
||||
### Step 2: Run the first scan
|
||||
|
||||
#### Scan all projects and clusters
|
||||
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
prowler mongodbatlas
|
||||
```
|
||||
|
||||
This command enumerates all projects accessible to the API key and scans every cluster.
|
||||
### Scanning a Specific Project
|
||||
|
||||
#### Scan a specific project
|
||||
|
||||
Add the `--atlas-project-id` flag when you only want to assess one project:
|
||||
To scan a specific project, add the following argument to the command above:
|
||||
|
||||
```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.
|
||||
|
||||
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 104 KiB |
@@ -58,7 +58,7 @@ Before you begin, ensure you have:
|
||||
|
||||
### Authentication
|
||||
|
||||
Prowler supports multiple authentication methods for OCI. For detailed authentication setup, see the [OCI Authentication Guide](./authentication).
|
||||
Prowler supports multiple authentication methods for OCI. For detailed authentication setup, see the [OCI Authentication Guide](./authentication.mdx).
|
||||
|
||||
**Note:** OCI Session Authentication and Config File Authentication both use the same `~/.oci/config` file. The difference is how the config file is generated - automatically via browser (session auth) or manually with API keys.
|
||||
|
||||
@@ -107,7 +107,7 @@ The easiest and most secure method is using OCI session authentication, which au
|
||||
|
||||
#### Alternative: Manual API Key Setup
|
||||
|
||||
If you prefer to manually generate API keys instead of using browser-based session authentication, see the detailed instructions in the [Authentication Guide](./authentication#config-file-authentication-manual-api-key-setup).
|
||||
If you prefer to manually generate API keys instead of using browser-based session authentication, see the detailed instructions in the [Authentication Guide](./authentication.mdx#config-file-authentication-manual-api-key-setup).
|
||||
|
||||
**Note:** Both methods use the same `~/.oci/config` file - the difference is that manual setup uses static API keys while session authentication uses temporary session tokens.
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
PROWLER_APP_API_KEY="pk_your_api_key_here"
|
||||
API_BASE_URL="https://api.prowler.com/api/v1"
|
||||
PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
PROWLER_MCP_TRANSPORT_MODE="stdio"
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
# 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
|
||||
@@ -2,26 +2,11 @@
|
||||
|
||||
All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
## [0.3.0] (Prowler v5.16.0)
|
||||
|
||||
### Added
|
||||
|
||||
- Add new MCP Server tools for Prowler Compliance Framework Management [(#9568)](https://github.com/prowler-cloud/prowler/pull/9568)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9542)
|
||||
- Standardize Prowler Hub and Docs tools format for AI optimization [(#9578)](https://github.com/prowler-cloud/prowler/pull/9578)
|
||||
|
||||
## [0.2.0] (Prowler v5.15.0)
|
||||
## [0.2.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
|
||||
- Remove all Prowler App MCP tools; and add new MCP Server tools for Prowler Findings and Compliance [(#9300)](https://github.com/prowler-cloud/prowler/pull/9300)
|
||||
- Add new MCP Server tools for Prowler Providers Management [(#9350)](https://github.com/prowler-cloud/prowler/pull/9350)
|
||||
- Add new MCP Server tools for Prowler Resources Management [(#9380)](https://github.com/prowler-cloud/prowler/pull/9380)
|
||||
- Add new MCP Server tools for Prowler Scans Management [(#9509)](https://github.com/prowler-cloud/prowler/pull/9509)
|
||||
- Add new MCP Server tools for Prowler Muting Management [(#9510)](https://github.com/prowler-cloud/prowler/pull/9510)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,60 +1,18 @@
|
||||
# Prowler MCP Server
|
||||
|
||||
**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.
|
||||
> ⚠️ **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.
|
||||
|
||||
> **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).
|
||||
Access the entire Prowler ecosystem through the Model Context Protocol (MCP). This server provides three main capabilities:
|
||||
|
||||
## Key Capabilities
|
||||
- **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
|
||||
|
||||
### Prowler Cloud and Prowler App (Self-Managed)
|
||||
## Quick Start with Hosted Server (Recommended)
|
||||
|
||||
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
|
||||
- **Compliance Reporting**: View compliance status across frameworks and drill into requirement-level details
|
||||
**The easiest way to use Prowler MCP is through our hosted server at `https://mcp.prowler.com/mcp`**
|
||||
|
||||
### 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 all supported Prowler providers
|
||||
|
||||
### 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
|
||||
No installation required! Just configure your MCP client:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -72,37 +30,70 @@ Prowler MCP Server can be used in three ways:
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Local STDIO Mode
|
||||
**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`
|
||||
|
||||
**Run the server locally on your machine**
|
||||
Get your API key at [Prowler Cloud](https://cloud.prowler.com) → Settings → API Keys
|
||||
|
||||
- Runs as a subprocess of your MCP client
|
||||
- Requires Python 3.12+ or Docker
|
||||
> **Benefits:** Always up-to-date, no maintenance, managed by Prowler team
|
||||
|
||||
### 3. Self-Hosted HTTP Mode
|
||||
## Local/Self-Hosted Installation
|
||||
|
||||
**Deploy your own remote MCP server**
|
||||
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`.
|
||||
|
||||
- Full control over deployment
|
||||
- Requires Python 3.12+ or Docker
|
||||
### Requirements
|
||||
|
||||
See the [Installation Guide](https://docs.prowler.com/getting-started/installation/prowler-mcp) for complete instructions.
|
||||
- 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)
|
||||
|
||||
## Quick Installation
|
||||
### Installation Methods
|
||||
|
||||
### Docker (Recommended)
|
||||
#### Option 1: Docker Hub (Recommended)
|
||||
|
||||
Pull the official image from Docker Hub:
|
||||
|
||||
```bash
|
||||
docker pull prowlercloud/prowler-mcp
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
### From Source
|
||||
Run in STDIO mode:
|
||||
```bash
|
||||
docker run --rm -i prowlercloud/prowler-mcp
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/prowler-cloud/prowler.git
|
||||
@@ -110,86 +101,400 @@ 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
|
||||
|
||||
For complete tool descriptions and parameters, see the [Tools Reference](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools).
|
||||
### Prowler Hub
|
||||
|
||||
### Tool Naming Convention
|
||||
All tools are exposed under the `prowler_hub` prefix.
|
||||
|
||||
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_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`: Full‑text 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).
|
||||
|
||||
## Architecture
|
||||
### Prowler Documentation
|
||||
|
||||
```
|
||||
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
|
||||
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"
|
||||
```
|
||||
|
||||
**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
|
||||
#### HTTP Mode Authentication
|
||||
|
||||
## Use Cases
|
||||
For HTTP mode (remote server), authentication is handled via Bearer tokens. The MCP server supports both JWT tokens and API keys:
|
||||
|
||||
The Prowler MCP Server enables powerful workflows through AI assistants:
|
||||
**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
|
||||
```
|
||||
|
||||
**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"
|
||||
**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 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?"
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**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?"
|
||||
The response will be a JWT token that you can use to [authenticate your MCP client](#http-mode-configuration-remote-server).
|
||||
|
||||
## Requirements
|
||||
### MCP Client Configuration
|
||||
|
||||
**For Prowler Cloud MCP Server:**
|
||||
- Prowler Cloud account and API key (only for Prowler Cloud/App features)
|
||||
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 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)
|
||||
#### STDIO Mode Configuration
|
||||
|
||||
> **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.
|
||||
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.
|
||||
|
||||
## Configuring MCP Hosts
|
||||
##### Using uvx (Direct Execution)
|
||||
|
||||
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.
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
##### Using Docker
|
||||
|
||||
For developers looking to extend the MCP server with new tools or features:
|
||||
```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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **[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
|
||||
#### HTTP Mode Configuration (Remote Server)
|
||||
|
||||
## Related Products
|
||||
For HTTP mode, you can configure your MCP client to connect to a remote Prowler MCP server.
|
||||
|
||||
- **[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
|
||||
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).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Pydantic models for Prowler App MCP Server."""
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.findings import (
|
||||
CheckMetadata,
|
||||
CheckRemediation,
|
||||
@@ -9,12 +10,6 @@ from prowler_mcp_server.prowler_app.models.findings import (
|
||||
FindingsOverview,
|
||||
SimplifiedFinding,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.models.muting import (
|
||||
DetailedMuteRule,
|
||||
MutelistResponse,
|
||||
MuteRulesListResponse,
|
||||
SimplifiedMuteRule,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base models
|
||||
@@ -26,9 +21,4 @@ __all__ = [
|
||||
"FindingsListResponse",
|
||||
"FindingsOverview",
|
||||
"SimplifiedFinding",
|
||||
# Muting models
|
||||
"DetailedMuteRule",
|
||||
"MutelistResponse",
|
||||
"MuteRulesListResponse",
|
||||
"SimplifiedMuteRule",
|
||||
]
|
||||
|
||||
@@ -27,19 +27,18 @@ class MinimalSerializerMixin(BaseModel):
|
||||
Dictionary with non-empty values only
|
||||
"""
|
||||
data = handler(self)
|
||||
return {k: v for k, v in data.items() if not self._should_exclude(k, v)}
|
||||
return {k: v for k, v in data.items() if not self._should_exclude(v)}
|
||||
|
||||
def _should_exclude(self, key: str, value: Any) -> bool:
|
||||
"""Determine if a key-value pair should be excluded from serialization.
|
||||
def _should_exclude(self, value: Any) -> bool:
|
||||
"""Determine if a value should be excluded from serialization.
|
||||
|
||||
Override this method in subclasses for custom exclusion logic.
|
||||
|
||||
Args:
|
||||
key: Field name
|
||||
value: Field value
|
||||
|
||||
Returns:
|
||||
True if the field should be excluded, False otherwise
|
||||
True if the value should be excluded, False otherwise
|
||||
"""
|
||||
# None values
|
||||
if value is None:
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
"""Pydantic models for simplified compliance responses."""
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SerializerFunctionWrapHandler,
|
||||
model_serializer,
|
||||
)
|
||||
|
||||
|
||||
class ComplianceRequirementAttribute(MinimalSerializerMixin, BaseModel):
|
||||
"""Requirement attributes including associated check IDs.
|
||||
|
||||
Used to map requirements to the checks that validate them.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(
|
||||
description="Requirement identifier within the framework (e.g., '1.1', '2.1.1')"
|
||||
)
|
||||
name: str = Field(default="", description="Human-readable name of the requirement")
|
||||
description: str = Field(
|
||||
default="", description="Detailed description of the requirement"
|
||||
)
|
||||
check_ids: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of Prowler check IDs that validate this requirement",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "ComplianceRequirementAttribute":
|
||||
"""Transform JSON:API compliance requirement attributes response to simplified format."""
|
||||
attributes = data.get("attributes", {})
|
||||
|
||||
# Extract check_ids from the nested attributes structure
|
||||
nested_attributes = attributes.get("attributes", {})
|
||||
check_ids = nested_attributes.get("check_ids", [])
|
||||
|
||||
return cls(
|
||||
id=attributes.get("id", data.get("id", "")),
|
||||
name=attributes.get("name", ""),
|
||||
description=attributes.get("description", ""),
|
||||
check_ids=check_ids if check_ids else [],
|
||||
)
|
||||
|
||||
|
||||
class ComplianceRequirementAttributesListResponse(BaseModel):
|
||||
"""Response for compliance requirement attributes list with check_ids mappings."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
requirements: list[ComplianceRequirementAttribute] = Field(
|
||||
description="List of requirements with their associated check IDs"
|
||||
)
|
||||
total_count: int = Field(description="Total number of requirements")
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
cls, response: dict
|
||||
) -> "ComplianceRequirementAttributesListResponse":
|
||||
"""Transform JSON:API response to simplified format."""
|
||||
data = response.get("data", [])
|
||||
|
||||
requirements = [
|
||||
ComplianceRequirementAttribute.from_api_response(item) for item in data
|
||||
]
|
||||
|
||||
return cls(
|
||||
requirements=requirements,
|
||||
total_count=len(requirements),
|
||||
)
|
||||
|
||||
|
||||
class ComplianceFrameworkSummary(MinimalSerializerMixin, BaseModel):
|
||||
"""Simplified compliance framework overview for list operations.
|
||||
|
||||
Used by get_compliance_overview() to show high-level compliance status
|
||||
per framework.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(description="Unique identifier for this compliance overview entry")
|
||||
compliance_id: str = Field(
|
||||
description="Compliance framework identifier (e.g., 'cis_1.5_aws', 'pci_dss_v4.0_aws')"
|
||||
)
|
||||
framework: str = Field(
|
||||
description="Human-readable framework name (e.g., 'CIS', 'PCI-DSS', 'HIPAA')"
|
||||
)
|
||||
version: str = Field(description="Framework version (e.g., '1.5', '4.0')")
|
||||
total_requirements: int = Field(
|
||||
default=0, description="Total number of requirements in this framework"
|
||||
)
|
||||
requirements_passed: int = Field(
|
||||
default=0, description="Number of requirements that passed"
|
||||
)
|
||||
requirements_failed: int = Field(
|
||||
default=0, description="Number of requirements that failed"
|
||||
)
|
||||
requirements_manual: int = Field(
|
||||
default=0, description="Number of requirements requiring manual verification"
|
||||
)
|
||||
|
||||
@property
|
||||
def pass_percentage(self) -> float:
|
||||
"""Calculate pass percentage based on passed requirements."""
|
||||
if self.total_requirements == 0:
|
||||
return 0.0
|
||||
return round((self.requirements_passed / self.total_requirements) * 100, 1)
|
||||
|
||||
@property
|
||||
def fail_percentage(self) -> float:
|
||||
"""Calculate fail percentage based on failed requirements."""
|
||||
if self.total_requirements == 0:
|
||||
return 0.0
|
||||
return round((self.requirements_failed / self.total_requirements) * 100, 1)
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
|
||||
"""Serialize with calculated percentages included."""
|
||||
data = handler(self)
|
||||
# Filter out None/empty values
|
||||
data = {k: v for k, v in data.items() if v is not None and v != "" and v != []}
|
||||
# Add calculated percentages
|
||||
data["pass_percentage"] = self.pass_percentage
|
||||
data["fail_percentage"] = self.fail_percentage
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "ComplianceFrameworkSummary":
|
||||
"""Transform JSON:API compliance overview response to simplified format."""
|
||||
attributes = data.get("attributes", {})
|
||||
|
||||
# The compliance_id field may be in attributes or use the "id" field from attributes
|
||||
compliance_id = attributes.get("id", data.get("id", ""))
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
compliance_id=compliance_id,
|
||||
framework=attributes.get("framework", ""),
|
||||
version=attributes.get("version", ""),
|
||||
total_requirements=attributes.get("total_requirements", 0),
|
||||
requirements_passed=attributes.get("requirements_passed", 0),
|
||||
requirements_failed=attributes.get("requirements_failed", 0),
|
||||
requirements_manual=attributes.get("requirements_manual", 0),
|
||||
)
|
||||
|
||||
|
||||
class ComplianceRequirement(MinimalSerializerMixin, BaseModel):
|
||||
"""Individual compliance requirement with its status.
|
||||
|
||||
Used by get_compliance_framework_state_details() to show requirement-level breakdown.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(
|
||||
description="Requirement identifier within the framework (e.g., '1.1', '2.1.1')"
|
||||
)
|
||||
description: str = Field(
|
||||
description="Human-readable description of the requirement"
|
||||
)
|
||||
status: Literal["FAIL", "PASS", "MANUAL"] = Field(
|
||||
description="Requirement status: FAIL (not compliant), PASS (compliant), MANUAL (requires manual verification)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "ComplianceRequirement":
|
||||
"""Transform JSON:API compliance requirement response to simplified format."""
|
||||
attributes = data.get("attributes", {})
|
||||
|
||||
return cls(
|
||||
id=attributes.get("id", data.get("id", "")),
|
||||
description=attributes.get("description", ""),
|
||||
status=attributes.get("status", "MANUAL"),
|
||||
)
|
||||
|
||||
|
||||
class ComplianceFrameworksListResponse(BaseModel):
|
||||
"""Response for compliance frameworks list with aggregated statistics."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
frameworks: list[ComplianceFrameworkSummary] = Field(
|
||||
description="List of compliance frameworks with their status"
|
||||
)
|
||||
total_count: int = Field(description="Total number of frameworks returned")
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict) -> "ComplianceFrameworksListResponse":
|
||||
"""Transform JSON:API response to simplified format."""
|
||||
data = response.get("data", [])
|
||||
|
||||
frameworks = [
|
||||
ComplianceFrameworkSummary.from_api_response(item) for item in data
|
||||
]
|
||||
|
||||
return cls(
|
||||
frameworks=frameworks,
|
||||
total_count=len(frameworks),
|
||||
)
|
||||
|
||||
|
||||
class ComplianceRequirementsListResponse(BaseModel):
|
||||
"""Response for compliance requirements list queries."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
requirements: list[ComplianceRequirement] = Field(
|
||||
description="List of requirements with their status"
|
||||
)
|
||||
total_count: int = Field(description="Total number of requirements")
|
||||
passed_count: int = Field(description="Number of requirements with PASS status")
|
||||
failed_count: int = Field(description="Number of requirements with FAIL status")
|
||||
manual_count: int = Field(description="Number of requirements with MANUAL status")
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict) -> "ComplianceRequirementsListResponse":
|
||||
"""Transform JSON:API response to simplified format."""
|
||||
data = response.get("data", [])
|
||||
|
||||
requirements = [ComplianceRequirement.from_api_response(item) for item in data]
|
||||
|
||||
# Calculate counts
|
||||
passed = sum(1 for r in requirements if r.status == "PASS")
|
||||
failed = sum(1 for r in requirements if r.status == "FAIL")
|
||||
manual = sum(1 for r in requirements if r.status == "MANUAL")
|
||||
|
||||
return cls(
|
||||
requirements=requirements,
|
||||
total_count=len(requirements),
|
||||
passed_count=passed,
|
||||
failed_count=failed,
|
||||
manual_count=manual,
|
||||
)
|
||||
@@ -19,17 +19,12 @@ class CheckRemediation(MinimalSerializerMixin, BaseModel):
|
||||
default=None,
|
||||
description="Terraform code snippet with best practices for remediation",
|
||||
)
|
||||
nativeiac: str | None = Field(
|
||||
default=None,
|
||||
description="Native Infrastructure as Code code snippet with best practices for remediation",
|
||||
recommendation_text: str | None = Field(
|
||||
default=None, description="Text description with best practices"
|
||||
)
|
||||
other: str | None = Field(
|
||||
recommendation_url: str | None = Field(
|
||||
default=None,
|
||||
description="Other remediation code snippet with best practices for remediation, usually used for web interfaces or other tools",
|
||||
)
|
||||
recommendation: str | None = Field(
|
||||
default=None,
|
||||
description="Text description with general best recommended practices to avoid the issue",
|
||||
description="URL to external remediation documentation",
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +33,9 @@ class CheckMetadata(MinimalSerializerMixin, BaseModel):
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
check_id: str = Field(
|
||||
description="Unique provider identifier for the security check (e.g., 's3_bucket_public_access')",
|
||||
)
|
||||
title: str = Field(
|
||||
description="Human-readable title of the security check",
|
||||
)
|
||||
@@ -61,9 +59,9 @@ class CheckMetadata(MinimalSerializerMixin, BaseModel):
|
||||
default=None,
|
||||
description="Remediation guidance including CLI commands and recommendations",
|
||||
)
|
||||
additional_urls: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of additional URLs related to the check",
|
||||
related_url: str | None = Field(
|
||||
default=None,
|
||||
description="URL to additional documentation or references",
|
||||
)
|
||||
categories: list[str] = Field(
|
||||
default_factory=list,
|
||||
@@ -81,23 +79,23 @@ class CheckMetadata(MinimalSerializerMixin, BaseModel):
|
||||
recommendation = remediation_data.get("recommendation", {})
|
||||
|
||||
remediation = CheckRemediation(
|
||||
cli=code["cli"],
|
||||
terraform=code["terraform"],
|
||||
nativeiac=code["nativeiac"],
|
||||
other=code["other"],
|
||||
recommendation=recommendation["text"],
|
||||
cli=code.get("cli"),
|
||||
terraform=code.get("terraform"),
|
||||
recommendation_text=recommendation.get("text"),
|
||||
recommendation_url=recommendation.get("url"),
|
||||
)
|
||||
|
||||
return cls(
|
||||
check_id=data["checkid"],
|
||||
title=data["checktitle"],
|
||||
description=data["description"],
|
||||
provider=data["provider"],
|
||||
risk=data["risk"],
|
||||
risk=data.get("risk"),
|
||||
service=data["servicename"],
|
||||
resource_type=data["resourcetype"],
|
||||
remediation=remediation,
|
||||
additional_urls=data["additionalurls"],
|
||||
categories=data["categories"],
|
||||
related_url=data.get("relatedurl"),
|
||||
categories=data.get("categories", []),
|
||||
)
|
||||
|
||||
|
||||
@@ -118,36 +116,35 @@ class SimplifiedFinding(MinimalSerializerMixin, BaseModel):
|
||||
severity: Literal["critical", "high", "medium", "low", "informational"] = Field(
|
||||
description="Severity level of the finding",
|
||||
)
|
||||
check_id: str = Field(
|
||||
description="ID of the security check that generated this finding",
|
||||
check_metadata: CheckMetadata = Field(
|
||||
description="Metadata about the security check that generated this finding",
|
||||
)
|
||||
status_extended: str = Field(
|
||||
description="Extended status information providing additional context",
|
||||
)
|
||||
delta: Literal["new", "changed"] | None = Field(
|
||||
default=None,
|
||||
delta: Literal["new", "changed"] = Field(
|
||||
description="Change status: 'new' (not seen before), 'changed' (modified since last scan), or None (unchanged)",
|
||||
)
|
||||
muted: bool | None = Field(
|
||||
default=None,
|
||||
muted: bool = Field(
|
||||
description="Whether this finding has been muted/suppressed by the user",
|
||||
)
|
||||
muted_reason: str | None = Field(
|
||||
muted_reason: str = Field(
|
||||
default=None,
|
||||
description="Reason provided when muting this finding",
|
||||
description="Reason provided when muting this finding (3-500 chars if muted)",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "SimplifiedFinding":
|
||||
"""Transform JSON:API finding response to simplified format."""
|
||||
attributes = data["attributes"]
|
||||
check_metadata = attributes["check_metadata"]
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
uid=attributes["uid"],
|
||||
status=attributes["status"],
|
||||
severity=attributes["severity"],
|
||||
check_id=attributes["check_metadata"]["checkid"],
|
||||
check_metadata=CheckMetadata.from_api_response(check_metadata),
|
||||
status_extended=attributes["status_extended"],
|
||||
delta=attributes["delta"],
|
||||
muted=attributes["muted"],
|
||||
@@ -182,9 +179,6 @@ class DetailedFinding(SimplifiedFinding):
|
||||
default_factory=list,
|
||||
description="List of UUIDs for cloud resources associated with this finding",
|
||||
)
|
||||
check_metadata: CheckMetadata = Field(
|
||||
description="Metadata about the security check that generated this finding",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "DetailedFinding":
|
||||
@@ -210,7 +204,6 @@ class DetailedFinding(SimplifiedFinding):
|
||||
uid=attributes["uid"],
|
||||
status=attributes["status"],
|
||||
severity=attributes["severity"],
|
||||
check_id=check_metadata["checkid"],
|
||||
check_metadata=CheckMetadata.from_api_response(check_metadata),
|
||||
status_extended=attributes.get("status_extended"),
|
||||
delta=attributes.get("delta"),
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
"""Pydantic models for simplified muting responses."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class MutelistResponse(MinimalSerializerMixin, BaseModel):
|
||||
"""Simplified mutelist response with Prowler configuration.
|
||||
|
||||
Represents a mutelist configuration that defines which findings
|
||||
should be automatically muted based on account patterns, check IDs, regions,
|
||||
resources, tags, and exceptions.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(
|
||||
description="Unique UUIDv4 identifier for this mutelist in Prowler database"
|
||||
)
|
||||
configuration: dict[str, Any] = Field(
|
||||
description="Mutelist configuration following Prowler format with nested structure: Mutelist → Accounts → Checks → Regions/Resources/Tags/Exceptions"
|
||||
)
|
||||
inserted_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when this mutelist was created",
|
||||
)
|
||||
updated_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when this mutelist was last modified",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "MutelistResponse":
|
||||
"""Transform JSON:API processor response to simplified format.
|
||||
|
||||
The configuration structure follows the Prowler mutelist format:
|
||||
{
|
||||
"Mutelist": {
|
||||
"Accounts": {
|
||||
"<account-pattern>": {
|
||||
"Checks": {
|
||||
"<check-id>": {
|
||||
"Regions": [...],
|
||||
"Resources": [...],
|
||||
"Tags": [...],
|
||||
"Exceptions": {...}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
attributes = data.get("attributes", {})
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
configuration=attributes.get("configuration", {}),
|
||||
inserted_at=attributes.get("inserted_at"),
|
||||
updated_at=attributes.get("updated_at"),
|
||||
)
|
||||
|
||||
|
||||
class SimplifiedMuteRule(MinimalSerializerMixin, BaseModel):
|
||||
"""Simplified mute rule for list/search operations.
|
||||
|
||||
Provides lightweight mute rule information without the full list of finding UIDs.
|
||||
Use this for listing and searching operations where you need basic rule information
|
||||
but don't need the complete list of affected findings.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(
|
||||
description="Unique UUIDv4 identifier for this mute rule in Prowler database"
|
||||
)
|
||||
name: str = Field(description="Human-readable name for this mute rule")
|
||||
reason: str = Field(description="Documented reason for muting these findings")
|
||||
enabled: bool = Field(
|
||||
description="Whether this mute rule is currently active and applying muting to findings"
|
||||
)
|
||||
finding_count: int = Field(
|
||||
description="Number of findings currently muted by this rule", ge=0
|
||||
)
|
||||
inserted_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when this mute rule was created",
|
||||
)
|
||||
updated_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when this mute rule was last modified",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedMuteRule":
|
||||
"""Transform JSON:API mute rule response to simplified format."""
|
||||
attributes = data.get("attributes", {})
|
||||
|
||||
# Calculate finding count from finding_uids list length
|
||||
finding_uids = attributes.get("finding_uids", [])
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=attributes["name"],
|
||||
reason=attributes["reason"],
|
||||
enabled=attributes["enabled"],
|
||||
finding_count=len(finding_uids),
|
||||
inserted_at=attributes.get("inserted_at"),
|
||||
updated_at=attributes.get("updated_at"),
|
||||
)
|
||||
|
||||
|
||||
class DetailedMuteRule(SimplifiedMuteRule):
|
||||
"""Detailed mute rule with complete information including finding UIDs.
|
||||
|
||||
Extends SimplifiedMuteRule with the full list of finding UIDs being muted and
|
||||
creator information (user/service account that created the rule).
|
||||
Use this when you need complete context about a specific mute rule, including
|
||||
all affected findings and audit trail information.
|
||||
"""
|
||||
|
||||
finding_uids: list[str] = Field(
|
||||
description="List of finding UIDs that are muted by this rule"
|
||||
)
|
||||
user_creator_id: str | None = Field(
|
||||
default=None,
|
||||
description="UUIDv4 identifier of the Prowler user from the tenant that created this rule",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "DetailedMuteRule":
|
||||
"""Transform JSON:API mute rule response to detailed format."""
|
||||
attributes = data.get("attributes", {})
|
||||
relationships = data.get("relationships", {})
|
||||
|
||||
# Extract creator information
|
||||
user_creator_id = None
|
||||
creator_data = relationships.get("created_by", {}).get("data")
|
||||
if creator_data:
|
||||
user_creator_id = creator_data.get("id")
|
||||
|
||||
finding_uids = attributes.get("finding_uids", [])
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=attributes["name"],
|
||||
reason=attributes["reason"],
|
||||
enabled=attributes["enabled"],
|
||||
finding_count=len(finding_uids),
|
||||
finding_uids=finding_uids,
|
||||
inserted_at=attributes.get("inserted_at"),
|
||||
updated_at=attributes.get("updated_at"),
|
||||
user_creator_id=user_creator_id,
|
||||
)
|
||||
|
||||
|
||||
class MuteRulesListResponse(BaseModel):
|
||||
"""Simplified response for mute rules list queries with pagination.
|
||||
|
||||
Contains a list of simplified mute rules and pagination metadata.
|
||||
Use this for paginated list/search operations to get multiple rules efficiently.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
mute_rules: list[SimplifiedMuteRule] = Field(
|
||||
description="List of simplified mute rules matching the query filters"
|
||||
)
|
||||
total_num_mute_rules: int = Field(
|
||||
description="Total number of mute rules matching the query across all pages",
|
||||
ge=0,
|
||||
)
|
||||
total_num_pages: int = Field(
|
||||
description="Total number of pages available for the query results", ge=0
|
||||
)
|
||||
current_page: int = Field(
|
||||
description="Current page number in the paginated results (1-indexed)", ge=1
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict[str, Any]) -> "MuteRulesListResponse":
|
||||
"""Transform JSON:API response to simplified format."""
|
||||
data = response.get("data", [])
|
||||
meta = response.get("meta", {})
|
||||
pagination = meta.get("pagination", {})
|
||||
|
||||
mute_rules = [SimplifiedMuteRule.from_api_response(item) for item in data]
|
||||
|
||||
return cls(
|
||||
mute_rules=mute_rules,
|
||||
total_num_mute_rules=pagination.get("count", 0),
|
||||
total_num_pages=pagination.get("pages", 1),
|
||||
current_page=pagination.get("page", 1),
|
||||
)
|
||||
@@ -1,134 +0,0 @@
|
||||
"""Pydantic models for simplified provider responses."""
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SimplifiedProvider(MinimalSerializerMixin, BaseModel):
|
||||
"""Simplified provider for list/search operations."""
|
||||
|
||||
id: str
|
||||
uid: str
|
||||
alias: str | None = None
|
||||
provider: str
|
||||
connected: bool | None = None
|
||||
secret_type: Literal["role", "service_account", "static"] | None = None
|
||||
|
||||
def _should_exclude(self, key: str, value: Any) -> bool:
|
||||
"""Override to always include connected and secret_type fields even when None."""
|
||||
# Always include these fields regardless of value (None has semantic meaning)
|
||||
if key == "connected" or key == "secret_type":
|
||||
return False
|
||||
# Use parent class logic for other fields
|
||||
return super()._should_exclude(key, value)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedProvider":
|
||||
"""Transform JSON:API provider response to simplified format."""
|
||||
attributes = data["attributes"]
|
||||
connection_data = attributes.get("connection", {})
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
uid=attributes["uid"],
|
||||
alias=attributes.get("alias"),
|
||||
provider=attributes["provider"],
|
||||
connected=connection_data.get("connected"),
|
||||
secret_type=None, # Will be populated separately via secret endpoint
|
||||
)
|
||||
|
||||
|
||||
class DetailedProvider(SimplifiedProvider):
|
||||
"""Detailed provider with complete information for deep analysis.
|
||||
|
||||
Extends SimplifiedProvider with temporal metadata and relationships.
|
||||
Use this when you need complete context about a specific provider.
|
||||
"""
|
||||
|
||||
inserted_at: str | None = None
|
||||
updated_at: str | None = None
|
||||
last_checked_at: str | None = None
|
||||
provider_group_ids: list[str] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "DetailedProvider":
|
||||
"""Transform JSON:API provider response to detailed format."""
|
||||
attributes = data["attributes"]
|
||||
connection_data = attributes.get("connection", {})
|
||||
relationships = data.get("relationships", {})
|
||||
|
||||
# Extract provider groups relationship
|
||||
provider_group_ids = None
|
||||
groups_data = relationships.get("provider_groups", {}).get("data", [])
|
||||
if groups_data:
|
||||
provider_group_ids = [group["id"] for group in groups_data]
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
uid=attributes["uid"],
|
||||
alias=attributes.get("alias"),
|
||||
provider=attributes["provider"],
|
||||
connected=connection_data.get("connected"),
|
||||
inserted_at=attributes.get("inserted_at"),
|
||||
updated_at=attributes.get("updated_at"),
|
||||
last_checked_at=connection_data.get("last_checked_at"),
|
||||
provider_group_ids=provider_group_ids,
|
||||
)
|
||||
|
||||
|
||||
class ProvidersListResponse(BaseModel):
|
||||
"""Simplified response for providers list queries."""
|
||||
|
||||
providers: list[SimplifiedProvider]
|
||||
total_num_providers: int
|
||||
total_num_pages: int
|
||||
current_page: int
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict[str, Any]) -> "ProvidersListResponse":
|
||||
"""Transform JSON:API response to simplified format."""
|
||||
data = response["data"]
|
||||
meta = response["meta"]
|
||||
pagination = meta["pagination"]
|
||||
|
||||
providers = [SimplifiedProvider.from_api_response(item) for item in data]
|
||||
|
||||
return cls(
|
||||
providers=providers,
|
||||
total_num_providers=pagination["count"],
|
||||
total_num_pages=pagination["pages"],
|
||||
current_page=pagination["page"],
|
||||
)
|
||||
|
||||
|
||||
class ProviderConnectionStatus(MinimalSerializerMixin, BaseModel):
|
||||
"""Result of provider connection operation."""
|
||||
|
||||
provider: DetailedProvider
|
||||
connected: Literal["connected", "failed", "not_tested"]
|
||||
error: str | None = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
provider_data: dict[str, Any],
|
||||
connection_status: dict[str, Any],
|
||||
) -> "ProviderConnectionStatus":
|
||||
"""Create connection status from provider data and connection test result."""
|
||||
|
||||
connected: str | None = connection_status.get("connected", None)
|
||||
|
||||
if connected is None:
|
||||
connected = "not_tested"
|
||||
elif connected:
|
||||
connected = "connected"
|
||||
else:
|
||||
connected = "failed"
|
||||
|
||||
return cls(
|
||||
provider=DetailedProvider.from_api_response(provider_data),
|
||||
connected=connected,
|
||||
error=connection_status.get("error", None),
|
||||
)
|
||||
@@ -1,137 +0,0 @@
|
||||
"""Pydantic models for simplified resources responses."""
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SimplifiedResource(MinimalSerializerMixin, BaseModel):
|
||||
"""Simplified resource with only LLM-relevant information for list operations."""
|
||||
|
||||
id: str
|
||||
uid: str
|
||||
name: str
|
||||
region: str
|
||||
service: str
|
||||
type: str
|
||||
failed_findings_count: int
|
||||
tags: dict[str, str] | None = None
|
||||
provider_id: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "SimplifiedResource":
|
||||
"""Transform JSON:API resource response to simplified format."""
|
||||
attributes = data["attributes"]
|
||||
relationships = data.get("relationships", {})
|
||||
|
||||
# Extract provider information from relationships if available
|
||||
provider_id = None
|
||||
provider_data = relationships.get("provider", {}).get("data", {})
|
||||
if provider_data:
|
||||
provider_id = provider_data["id"]
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
uid=attributes["uid"],
|
||||
name=attributes["name"],
|
||||
region=attributes["region"],
|
||||
service=attributes["service"],
|
||||
type=attributes["type"],
|
||||
failed_findings_count=attributes["failed_findings_count"],
|
||||
tags=attributes["tags"],
|
||||
provider_id=provider_id,
|
||||
)
|
||||
|
||||
|
||||
class DetailedResource(SimplifiedResource):
|
||||
"""Detailed resource with comprehensive information for deep analysis.
|
||||
|
||||
Extends SimplifiedResource with tags, metadata, configuration details,
|
||||
temporal information, and relationships.
|
||||
Use this when you need complete context about a specific resource.
|
||||
"""
|
||||
|
||||
metadata: str | None = None
|
||||
partition: str | None = None
|
||||
inserted_at: str
|
||||
updated_at: str
|
||||
finding_ids: list[str] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict) -> "DetailedResource":
|
||||
"""Transform JSON:API resource response to detailed format."""
|
||||
attributes = data["attributes"]
|
||||
relationships = data.get("relationships", {})
|
||||
|
||||
# Parse findings relationship
|
||||
finding_ids = None
|
||||
findings_data = relationships.get("findings", {}).get("data", [])
|
||||
if findings_data:
|
||||
finding_ids = [f["id"] for f in findings_data]
|
||||
|
||||
# Extract provider information from relationships if available
|
||||
provider_id = None
|
||||
provider_data = relationships.get("provider", {}).get("data", {})
|
||||
if provider_data:
|
||||
provider_id = provider_data["id"]
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
uid=attributes["uid"],
|
||||
name=attributes["name"],
|
||||
region=attributes["region"],
|
||||
service=attributes["service"],
|
||||
type=attributes["type"],
|
||||
failed_findings_count=attributes["failed_findings_count"],
|
||||
tags=attributes["tags"],
|
||||
metadata=attributes["metadata"],
|
||||
partition=attributes["partition"],
|
||||
inserted_at=attributes["inserted_at"],
|
||||
updated_at=attributes["updated_at"],
|
||||
finding_ids=finding_ids,
|
||||
provider_id=provider_id,
|
||||
)
|
||||
|
||||
|
||||
class ResourcesListResponse(BaseModel):
|
||||
"""Simplified response for resources list queries."""
|
||||
|
||||
resources: list[SimplifiedResource]
|
||||
total_num_resources: int
|
||||
total_num_pages: int
|
||||
current_page: int
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict) -> "ResourcesListResponse":
|
||||
"""Transform JSON:API response to simplified format."""
|
||||
data = response["data"]
|
||||
meta = response["meta"]
|
||||
pagination = meta["pagination"]
|
||||
|
||||
resources = [SimplifiedResource.from_api_response(item) for item in data]
|
||||
|
||||
return cls(
|
||||
resources=resources,
|
||||
total_num_resources=pagination["count"],
|
||||
total_num_pages=pagination["pages"],
|
||||
current_page=pagination["page"],
|
||||
)
|
||||
|
||||
|
||||
class ResourcesMetadataResponse(BaseModel):
|
||||
"""Metadata response with unique filter values for resource discovery."""
|
||||
|
||||
services: list[str] | None = None
|
||||
regions: list[str] | None = None
|
||||
types: list[str] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict) -> "ResourcesMetadataResponse":
|
||||
"""Transform JSON:API metadata response to simplified format."""
|
||||
data = response["data"]
|
||||
attributes = data["attributes"]
|
||||
|
||||
return cls(
|
||||
services=attributes.get("services"),
|
||||
regions=attributes.get("regions"),
|
||||
types=attributes.get("types"),
|
||||
)
|
||||
@@ -1,222 +0,0 @@
|
||||
"""Data models for Prowler scans.
|
||||
|
||||
This module provides Pydantic models for representing Prowler security scans
|
||||
with two-tier complexity:
|
||||
- SimplifiedScan: For list operations with essential fields
|
||||
- DetailedScan: Extends simplified with additional operational fields
|
||||
|
||||
All models inherit from MinimalSerializerMixin to exclude None/empty values
|
||||
for optimal LLM token usage.
|
||||
"""
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class SimplifiedScan(MinimalSerializerMixin, BaseModel):
|
||||
"""Simplified scan representation for list operations.
|
||||
|
||||
Includes core scan fields for efficient overview.
|
||||
Used by list_scans() tool.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
id: str = Field(
|
||||
description="Unique UUIDv4 identifier for this scan in Prowler database"
|
||||
)
|
||||
name: str | None = Field(
|
||||
default=None,
|
||||
description="Optional custom name for the scan to help identify it",
|
||||
)
|
||||
trigger: Literal["manual", "scheduled"] = Field(
|
||||
description="How the scan was initiated: 'manual' (user-triggered) or 'scheduled' (automated)"
|
||||
)
|
||||
state: Literal[
|
||||
"available", "scheduled", "executing", "completed", "failed", "cancelled"
|
||||
] = Field(
|
||||
description="Current state of the scan: available, scheduled, executing, completed, failed, or cancelled"
|
||||
)
|
||||
started_at: str | None = Field(
|
||||
default=None, description="ISO 8601 timestamp when the scan started execution"
|
||||
)
|
||||
completed_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when the scan finished (completed or failed)",
|
||||
)
|
||||
provider_id: str = Field(
|
||||
description="UUIDv4 identifier of the provider this scan is associated with"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedScan":
|
||||
"""Transform JSON:API scan response to simplified model.
|
||||
|
||||
Args:
|
||||
data: Scan data from API response['data'] (single item or list item)
|
||||
|
||||
Returns:
|
||||
SimplifiedScan instance
|
||||
"""
|
||||
attributes = data["attributes"]
|
||||
relationships = data.get("relationships", {})
|
||||
|
||||
provider_id = relationships.get("provider", {}).get("data", {}).get("id", None)
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=attributes.get("name"),
|
||||
trigger=attributes["trigger"],
|
||||
state=attributes["state"],
|
||||
started_at=attributes.get("started_at"),
|
||||
completed_at=attributes.get("completed_at"),
|
||||
provider_id=provider_id,
|
||||
)
|
||||
|
||||
|
||||
class DetailedScan(SimplifiedScan):
|
||||
"""Detailed scan representation with full operational data.
|
||||
|
||||
Extends SimplifiedScan with progress, duration, resources, and relationships.
|
||||
Used by get_scan() and create_scan() tools.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
progress: int | None = Field(
|
||||
default=None, description="Scan completion progress as percentage (0-100)"
|
||||
)
|
||||
duration: int | None = Field(
|
||||
default=None,
|
||||
description="Total scan duration in seconds from start to completion",
|
||||
)
|
||||
unique_resource_count: int | None = Field(
|
||||
default=None,
|
||||
description="Number of unique cloud resources discovered during the scan",
|
||||
)
|
||||
inserted_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when the scan was created in the database",
|
||||
)
|
||||
scheduled_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp when the scan was scheduled to run",
|
||||
)
|
||||
next_scan_at: str | None = Field(
|
||||
default=None,
|
||||
description="ISO 8601 timestamp for the next scheduled scan (for recurring scans)",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "DetailedScan":
|
||||
"""Transform JSON:API scan response to detailed model.
|
||||
|
||||
Args:
|
||||
data: Scan data from API response['data']
|
||||
|
||||
Returns:
|
||||
DetailedScan instance with all fields populated
|
||||
"""
|
||||
attributes = data["attributes"]
|
||||
relationships = data.get("relationships", {})
|
||||
|
||||
# Extract provider ID from relationship
|
||||
provider_rel = relationships.get("provider", {}).get("data", {})
|
||||
provider_id = provider_rel.get("id", "")
|
||||
|
||||
# Extract task relationship
|
||||
task_rel = relationships.get("task", {}).get("data")
|
||||
task_id = task_rel.get("id") if task_rel else None
|
||||
|
||||
# Extract processor relationship
|
||||
processor_rel = relationships.get("processor", {}).get("data")
|
||||
processor_id = processor_rel.get("id") if processor_rel else None
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=attributes.get("name"),
|
||||
trigger=attributes["trigger"],
|
||||
state=attributes["state"],
|
||||
started_at=attributes.get("started_at"),
|
||||
completed_at=attributes.get("completed_at"),
|
||||
provider_id=provider_id,
|
||||
progress=attributes.get("progress"),
|
||||
duration=attributes.get("duration"),
|
||||
unique_resource_count=attributes.get("unique_resource_count"),
|
||||
inserted_at=attributes.get("inserted_at"),
|
||||
scheduled_at=attributes.get("scheduled_at"),
|
||||
next_scan_at=attributes.get("next_scan_at"),
|
||||
task_id=task_id,
|
||||
processor_id=processor_id,
|
||||
)
|
||||
|
||||
|
||||
class ScansListResponse(BaseModel):
|
||||
"""Response model for list_scans() with pagination metadata.
|
||||
|
||||
Follows established pattern from FindingsListResponse and ProvidersListResponse.
|
||||
"""
|
||||
|
||||
scans: list[SimplifiedScan]
|
||||
total_num_scans: int
|
||||
total_num_pages: int
|
||||
current_page: int
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: dict[str, Any]) -> "ScansListResponse":
|
||||
"""Transform JSON:API list response to scans list with pagination.
|
||||
|
||||
Args:
|
||||
response: Full API response with data and meta
|
||||
|
||||
Returns:
|
||||
ScansListResponse with simplified scans and pagination metadata
|
||||
"""
|
||||
data = response.get("data", [])
|
||||
meta = response.get("meta", {})
|
||||
pagination = meta.get("pagination", {})
|
||||
|
||||
# Transform each scan
|
||||
scans = [SimplifiedScan.from_api_response(item) for item in data]
|
||||
|
||||
return cls(
|
||||
scans=scans,
|
||||
total_num_scans=pagination.get("count", 0),
|
||||
total_num_pages=pagination.get("pages", 0),
|
||||
current_page=pagination.get("page", 1),
|
||||
)
|
||||
|
||||
|
||||
class ScanCreationResult(MinimalSerializerMixin, BaseModel):
|
||||
"""Result of scan creation operation.
|
||||
|
||||
Used by trigger_scan() to communicate the outcome of scan creation.
|
||||
Status indicates whether scan was created successfully or failed.
|
||||
"""
|
||||
|
||||
scan: DetailedScan | None = Field(
|
||||
default=None,
|
||||
description="Detailed scan information if creation succeeded, None otherwise",
|
||||
)
|
||||
status: Literal["success", "failed"] = Field(
|
||||
description="Outcome of scan creation: success (scan created successfully) or failed (error)"
|
||||
)
|
||||
message: str = Field(
|
||||
description="Human-readable message describing the scan creation result"
|
||||
)
|
||||
|
||||
|
||||
class ScheduleCreationResult(MinimalSerializerMixin, BaseModel):
|
||||
"""Result of async schedule creation operation.
|
||||
|
||||
Used by schedule_daily_scan() to communicate scheduling outcome.
|
||||
"""
|
||||
|
||||
scheduled: bool = Field(
|
||||
description="Whether the daily scan schedule was created successfully"
|
||||
)
|
||||
message: str = Field(
|
||||
description="Human-readable message describing the scheduling result"
|
||||
)
|
||||
@@ -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("/findings")
|
||||
response = await self.api_client.get("/api/v1/findings")
|
||||
return response
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
"""Compliance framework tools for Prowler App MCP Server.
|
||||
|
||||
This module provides tools for viewing compliance status and requirement details
|
||||
across all cloud providers.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.compliance import (
|
||||
ComplianceFrameworksListResponse,
|
||||
ComplianceRequirementAttributesListResponse,
|
||||
ComplianceRequirementsListResponse,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ComplianceTools(BaseTool):
|
||||
"""Tools for compliance framework operations.
|
||||
|
||||
Provides tools for:
|
||||
- get_compliance_overview: Get high-level compliance status across all frameworks
|
||||
- get_compliance_framework_state_details: Get detailed requirement-level breakdown for a specific framework
|
||||
"""
|
||||
|
||||
async def _get_latest_scan_id_for_provider(self, provider_id: str) -> str:
|
||||
"""Get the latest completed scan_id for a given provider.
|
||||
|
||||
Args:
|
||||
provider_id: Prowler's internal UUID for the provider
|
||||
|
||||
Returns:
|
||||
The scan_id of the latest completed scan for the provider.
|
||||
|
||||
Raises:
|
||||
ValueError: If no completed scans are found for the provider.
|
||||
"""
|
||||
scan_params = {
|
||||
"filter[provider]": provider_id,
|
||||
"filter[state]": "completed",
|
||||
"sort": "-inserted_at",
|
||||
"page[size]": 1,
|
||||
"page[number]": 1,
|
||||
}
|
||||
clean_scan_params = self.api_client.build_filter_params(scan_params)
|
||||
scans_response = await self.api_client.get("/scans", params=clean_scan_params)
|
||||
|
||||
scans_data = scans_response.get("data", [])
|
||||
if not scans_data:
|
||||
raise ValueError(
|
||||
f"No completed scans found for provider {provider_id}. "
|
||||
"Run a scan first using prowler_app_trigger_scan."
|
||||
)
|
||||
|
||||
scan_id = scans_data[0]["id"]
|
||||
return scan_id
|
||||
|
||||
async def get_compliance_overview(
|
||||
self,
|
||||
scan_id: str | None = Field(
|
||||
default=None,
|
||||
description="UUID of a specific scan to get compliance data for. Required if provider_id is not specified. Use `prowler_app_list_scans` to find scan IDs.",
|
||||
),
|
||||
provider_id: str | None = Field(
|
||||
default=None,
|
||||
description="Prowler's internal UUID (v4) for a specific provider. If provided without scan_id, the tool will automatically find the latest completed scan for this provider. Use `prowler_app_search_providers` tool to find provider IDs.",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Get high-level compliance overview across all frameworks for a specific scan.
|
||||
|
||||
This tool provides a HIGH-LEVEL OVERVIEW of compliance status across all frameworks.
|
||||
Use this when you need to understand overall compliance posture before drilling into
|
||||
specific framework details.
|
||||
|
||||
You have two options to specify the scan context:
|
||||
1. Provide a specific scan_id to get compliance data for that scan.
|
||||
2. Provide a provider_id to get compliance data from the latest completed scan for that provider.
|
||||
|
||||
The markdown report includes:
|
||||
|
||||
1. Summary Statistics:
|
||||
- Total number of compliance frameworks evaluated
|
||||
- Overall compliance metrics across all frameworks
|
||||
|
||||
2. Per-Framework Breakdown:
|
||||
- Framework name, version, and compliance ID
|
||||
- Requirements passed/failed/manual counts
|
||||
- Pass percentage for quick assessment
|
||||
|
||||
Workflow:
|
||||
1. Use this tool to get an overview of all compliance frameworks
|
||||
2. Use prowler_app_get_compliance_framework_state_details with a specific compliance_id to see which requirements failed
|
||||
"""
|
||||
if not scan_id and not provider_id:
|
||||
return {
|
||||
"error": "Either scan_id or provider_id must be provided. Use prowler_app_search_providers to find provider IDs or prowler_app_list_scans to find scan IDs."
|
||||
}
|
||||
elif scan_id and provider_id:
|
||||
return {
|
||||
"error": "Provide either scan_id or provider_id, not both. To get compliance data for a specific scan, use scan_id. To get data for the latest scan of a provider, use provider_id."
|
||||
}
|
||||
elif not scan_id and provider_id:
|
||||
try:
|
||||
scan_id = await self._get_latest_scan_id_for_provider(provider_id)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
params: dict[str, Any] = {"filter[scan_id]": scan_id}
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
# Get API response
|
||||
api_response = await self.api_client.get(
|
||||
"/compliance-overviews", params=clean_params
|
||||
)
|
||||
frameworks_response = ComplianceFrameworksListResponse.from_api_response(
|
||||
api_response
|
||||
)
|
||||
|
||||
# Build markdown report
|
||||
frameworks = frameworks_response.frameworks
|
||||
total_frameworks = frameworks_response.total_count
|
||||
|
||||
if total_frameworks == 0:
|
||||
return {"report": "# Compliance Overview\n\nNo compliance frameworks found"}
|
||||
|
||||
# Calculate aggregate statistics
|
||||
total_requirements = sum(f.total_requirements for f in frameworks)
|
||||
total_passed = sum(f.requirements_passed for f in frameworks)
|
||||
total_failed = sum(f.requirements_failed for f in frameworks)
|
||||
total_manual = sum(f.requirements_manual for f in frameworks)
|
||||
overall_pass_pct = (
|
||||
round((total_passed / total_requirements) * 100, 1)
|
||||
if total_requirements > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
# Build report
|
||||
report_lines = [
|
||||
"# Compliance Overview",
|
||||
"",
|
||||
"## Summary Statistics",
|
||||
f"- **Frameworks Evaluated**: {total_frameworks}",
|
||||
f"- **Total Requirements**: {total_requirements:,}",
|
||||
f"- **Passed**: {total_passed:,} ({overall_pass_pct}%)",
|
||||
f"- **Failed**: {total_failed:,}",
|
||||
f"- **Manual Review**: {total_manual:,}",
|
||||
"",
|
||||
"## Framework Breakdown",
|
||||
"",
|
||||
]
|
||||
|
||||
# Sort frameworks by fail count (most failures first)
|
||||
sorted_frameworks = sorted(
|
||||
frameworks, key=lambda f: f.requirements_failed, reverse=True
|
||||
)
|
||||
|
||||
for fw in sorted_frameworks:
|
||||
status_indicator = "PASS" if fw.requirements_failed == 0 else "FAIL"
|
||||
|
||||
report_lines.append(f"### {fw.framework} {fw.version}")
|
||||
report_lines.append(f"- **Compliance ID**: `{fw.compliance_id}`")
|
||||
report_lines.append(f"- **Status**: {status_indicator}")
|
||||
report_lines.append(
|
||||
f"- **Requirements**: {fw.requirements_passed}/{fw.total_requirements} passed ({fw.pass_percentage}%)"
|
||||
)
|
||||
if fw.requirements_failed > 0:
|
||||
report_lines.append(f"- **Failed**: {fw.requirements_failed}")
|
||||
if fw.requirements_manual > 0:
|
||||
report_lines.append(f"- **Manual Review**: {fw.requirements_manual}")
|
||||
report_lines.append("")
|
||||
|
||||
return {"report": "\n".join(report_lines)}
|
||||
|
||||
async def _get_requirement_check_ids_mapping(
|
||||
self, compliance_id: str
|
||||
) -> dict[str, list[str]]:
|
||||
"""Get mapping of requirement IDs to their associated check IDs.
|
||||
|
||||
Args:
|
||||
compliance_id: The compliance framework ID.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping requirement ID to list of check IDs.
|
||||
"""
|
||||
params: dict[str, Any] = {
|
||||
"filter[compliance_id]": compliance_id,
|
||||
"fields[compliance-requirements-attributes]": "id,attributes",
|
||||
}
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
api_response = await self.api_client.get(
|
||||
"/compliance-overviews/attributes", params=clean_params
|
||||
)
|
||||
attributes_response = (
|
||||
ComplianceRequirementAttributesListResponse.from_api_response(api_response)
|
||||
)
|
||||
|
||||
# Build mapping: requirement_id -> [check_ids]
|
||||
return {req.id: req.check_ids for req in attributes_response.requirements}
|
||||
|
||||
async def _get_failed_finding_ids_for_checks(
|
||||
self,
|
||||
check_ids: list[str],
|
||||
scan_id: str,
|
||||
) -> list[str]:
|
||||
"""Get all failed finding IDs for a list of check IDs.
|
||||
|
||||
Args:
|
||||
check_ids: List of Prowler check IDs.
|
||||
scan_id: The scan ID to filter findings.
|
||||
|
||||
Returns:
|
||||
List of all finding IDs with FAIL status.
|
||||
"""
|
||||
if not check_ids:
|
||||
return []
|
||||
|
||||
all_finding_ids: list[str] = []
|
||||
page_number = 1
|
||||
page_size = 100
|
||||
|
||||
while True:
|
||||
# Query findings endpoint with check_id filter and FAIL status
|
||||
params: dict[str, Any] = {
|
||||
"filter[scan]": scan_id,
|
||||
"filter[check_id__in]": ",".join(check_ids),
|
||||
"filter[status]": "FAIL",
|
||||
"fields[findings]": "uid",
|
||||
"page[size]": page_size,
|
||||
"page[number]": page_number,
|
||||
}
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
api_response = await self.api_client.get("/findings", params=clean_params)
|
||||
|
||||
findings = api_response.get("data", [])
|
||||
if not findings:
|
||||
break
|
||||
|
||||
all_finding_ids.extend([f["id"] for f in findings])
|
||||
|
||||
# Check if we've reached the last page
|
||||
if len(findings) < page_size:
|
||||
break
|
||||
|
||||
page_number += 1
|
||||
|
||||
return all_finding_ids
|
||||
|
||||
async def get_compliance_framework_state_details(
|
||||
self,
|
||||
compliance_id: str = Field(
|
||||
description="Compliance framework ID to get details for (e.g., 'cis_1.5_aws', 'pci_dss_v4.0_aws'). You can get compliance IDs from prowler_app_get_compliance_overview or consulting Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
|
||||
),
|
||||
scan_id: str | None = Field(
|
||||
default=None,
|
||||
description="UUID of a specific scan to get compliance data for. Required if provider_id is not specified.",
|
||||
),
|
||||
provider_id: str | None = Field(
|
||||
default=None,
|
||||
description="Prowler's internal UUID (v4) for a specific provider. If provided without scan_id, the tool will automatically find the latest completed scan for this provider. Use `prowler_app_search_providers` tool to find provider IDs.",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Get detailed requirement-level breakdown for a specific compliance framework.
|
||||
|
||||
IMPORTANT: This tool returns DETAILED requirement information for a single compliance framework,
|
||||
focusing on FAILED requirements and their associated FAILED finding IDs.
|
||||
Use this after prowler_app_get_compliance_overview to drill down into specific frameworks.
|
||||
|
||||
The markdown report includes:
|
||||
|
||||
1. Framework Summary:
|
||||
- Compliance ID and scan ID used
|
||||
- Overall pass/fail/manual counts
|
||||
|
||||
2. Failed Requirements Breakdown:
|
||||
- Each failed requirement's ID and description
|
||||
- Associated failed finding IDs for each failed requirement
|
||||
- Use prowler_app_get_finding_details with these finding IDs for more details and remediation guidance
|
||||
|
||||
Default behavior:
|
||||
- Requires either scan_id OR provider_id
|
||||
- With provider_id (no scan_id): Automatically finds the latest completed scan for that provider
|
||||
- With scan_id: Uses that specific scan's compliance data
|
||||
- Only shows failed requirements with their associated failed finding IDs
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_get_compliance_overview to identify frameworks with failures
|
||||
2. Use this tool with the compliance_id to see failed requirements and their finding IDs
|
||||
3. Use prowler_app_get_finding_details with the finding IDs to get remediation guidance
|
||||
"""
|
||||
# Validate that either scan_id or provider_id is provided
|
||||
if not scan_id and not provider_id:
|
||||
return {
|
||||
"error": "Either scan_id or provider_id must be provided. Use prowler_app_search_providers to find provider IDs or prowler_app_list_scans to find scan IDs."
|
||||
}
|
||||
|
||||
# Resolve provider_id to latest scan_id if needed
|
||||
resolved_scan_id = scan_id
|
||||
if not scan_id and provider_id:
|
||||
try:
|
||||
resolved_scan_id = await self._get_latest_scan_id_for_provider(
|
||||
provider_id
|
||||
)
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# Build params for requirements endpoint
|
||||
params: dict[str, Any] = {
|
||||
"filter[scan_id]": resolved_scan_id,
|
||||
"filter[compliance_id]": compliance_id,
|
||||
}
|
||||
|
||||
params["fields[compliance-requirements-details]"] = "id,description,status"
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
# Get API response
|
||||
api_response = await self.api_client.get(
|
||||
"/compliance-overviews/requirements", params=clean_params
|
||||
)
|
||||
requirements_response = ComplianceRequirementsListResponse.from_api_response(
|
||||
api_response
|
||||
)
|
||||
|
||||
requirements = requirements_response.requirements
|
||||
|
||||
if not requirements:
|
||||
return {
|
||||
"report": f"# Compliance Framework Details\n\n**Compliance ID**: `{compliance_id}`\n\nNo requirements found for this compliance framework and scan combination."
|
||||
}
|
||||
|
||||
# Get failed requirements
|
||||
failed_reqs = [r for r in requirements if r.status == "FAIL"]
|
||||
|
||||
# Get requirement -> check_ids mapping from attributes endpoint
|
||||
requirement_check_mapping: dict[str, list[str]] = {}
|
||||
if failed_reqs:
|
||||
requirement_check_mapping = await self._get_requirement_check_ids_mapping(
|
||||
compliance_id
|
||||
)
|
||||
|
||||
# For each failed requirement, get the failed finding IDs
|
||||
failed_req_findings: dict[str, list[str]] = {}
|
||||
for req in failed_reqs:
|
||||
check_ids = requirement_check_mapping.get(req.id, [])
|
||||
if check_ids:
|
||||
finding_ids = await self._get_failed_finding_ids_for_checks(
|
||||
check_ids, resolved_scan_id
|
||||
)
|
||||
failed_req_findings[req.id] = finding_ids
|
||||
|
||||
# Calculate counts
|
||||
total_count = len(requirements)
|
||||
passed_count = sum(1 for r in requirements if r.status == "PASS")
|
||||
failed_count = len(failed_reqs)
|
||||
manual_count = sum(1 for r in requirements if r.status == "MANUAL")
|
||||
|
||||
# Build markdown report
|
||||
pass_pct = (
|
||||
round((passed_count / total_count) * 100, 1) if total_count > 0 else 0
|
||||
)
|
||||
|
||||
report_lines = [
|
||||
"# Compliance Framework Details",
|
||||
"",
|
||||
f"**Compliance ID**: `{compliance_id}`",
|
||||
f"**Scan ID**: `{resolved_scan_id}`",
|
||||
"",
|
||||
"## Summary",
|
||||
f"- **Total Requirements**: {total_count}",
|
||||
f"- **Passed**: {passed_count} ({pass_pct}%)",
|
||||
f"- **Failed**: {failed_count}",
|
||||
f"- **Manual Review**: {manual_count}",
|
||||
"",
|
||||
]
|
||||
|
||||
# Show failed requirements with their finding IDs (most actionable)
|
||||
if failed_reqs:
|
||||
report_lines.append("## Failed Requirements")
|
||||
report_lines.append("")
|
||||
for req in failed_reqs:
|
||||
report_lines.append(f"### {req.id}")
|
||||
report_lines.append(f"**Description**: {req.description}")
|
||||
finding_ids = failed_req_findings.get(req.id, [])
|
||||
if finding_ids:
|
||||
report_lines.append(f"**Failed Finding IDs** ({len(finding_ids)}):")
|
||||
for fid in finding_ids:
|
||||
report_lines.append(f" - `{fid}`")
|
||||
else:
|
||||
report_lines.append("**Failed Finding IDs**: None found")
|
||||
report_lines.append("")
|
||||
report_lines.append(
|
||||
"*Use `prowler_app_get_finding_details` with these finding IDs to get remediation guidance.*"
|
||||
)
|
||||
report_lines.append("")
|
||||
|
||||
if manual_count > 0:
|
||||
manual_reqs = [r for r in requirements if r.status == "MANUAL"]
|
||||
report_lines.append("## Requirements Requiring Manual Review")
|
||||
report_lines.append("")
|
||||
for req in manual_reqs:
|
||||
report_lines.append(f"- **{req.id}**: {req.description}")
|
||||
report_lines.append("")
|
||||
|
||||
return {"report": "\n".join(report_lines)}
|
||||
@@ -6,23 +6,22 @@ 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):
|
||||
"""Tools for security findings operations.
|
||||
|
||||
Provides tools for:
|
||||
- search_security_findings: Fast and lightweight searching across findings
|
||||
- get_finding_details: Get complete details for a specific finding
|
||||
- get_findings_overview: Get aggregate statistics and trends across all findings
|
||||
- Searching and filtering security findings
|
||||
- Getting detailed finding information
|
||||
- Viewing findings overview/statistics
|
||||
"""
|
||||
|
||||
async def search_security_findings(
|
||||
@@ -91,27 +90,27 @@ class FindingsTools(BaseTool):
|
||||
) -> dict[str, Any]:
|
||||
"""Search and filter security findings across all cloud providers with rich filtering capabilities.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT findings. Use this for fast searching and filtering across many findings.
|
||||
For complete details use prowler_app_get_finding_details on specific findings.
|
||||
This is the primary tool for browsing and filtering security findings. Returns lightweight findings
|
||||
optimized for searching across large result sets. For detailed information about a specific finding,
|
||||
use get_finding_details.
|
||||
|
||||
Default behavior:
|
||||
- Returns latest findings from most recent scans (no date parameters needed)
|
||||
- Filters to FAIL status only (security issues found)
|
||||
- Returns 50 results per page
|
||||
- Returns 100 results per page
|
||||
|
||||
Date filtering:
|
||||
- Without dates: queries findings from the most recent completed scan across all providers (most efficient)
|
||||
- With dates: queries historical findings (2-day maximum range between date_from and date_to)
|
||||
- Without dates: queries findings from the most recent completed scan across all providers (most efficient). This returns the latest snapshot of findings, not a time-based query.
|
||||
- With dates: queries historical findings (2-day maximum range)
|
||||
|
||||
Each finding includes:
|
||||
- Core identification: id (UUID for get_finding_details), uid, check_id
|
||||
- Security context: status (FAIL/PASS/MANUAL), severity (critical/high/medium/low/informational)
|
||||
- State tracking: delta (new/changed/unchanged), muted (boolean), muted_reason
|
||||
- Extended details: status_extended with additional context
|
||||
- Core identification: id, uid, check_id
|
||||
- Security context: status, severity, check_metadata (title, description, remediation)
|
||||
- State tracking: delta (new/changed), muted status
|
||||
- Extended details: status_extended for additional context
|
||||
|
||||
Workflow:
|
||||
1. Use this tool to search and filter findings by severity, status, provider, service, region, etc.
|
||||
2. Use prowler_app_get_finding_details with the finding 'id' to get complete information about the finding
|
||||
Returns:
|
||||
Paginated list of simplified findings with total count and pagination metadata
|
||||
"""
|
||||
# Validate page_size parameter
|
||||
self.api_client.validate_page_size(page_size)
|
||||
@@ -123,11 +122,11 @@ class FindingsTools(BaseTool):
|
||||
|
||||
if date_range is None:
|
||||
# No dates provided - use latest findings endpoint
|
||||
endpoint = "/findings/latest"
|
||||
endpoint = "/api/v1/findings/latest"
|
||||
params = {}
|
||||
else:
|
||||
# Dates provided - use historical findings endpoint
|
||||
endpoint = "/findings"
|
||||
endpoint = "/api/v1/findings"
|
||||
params = {
|
||||
"filter[inserted_at__gte]": date_range[0],
|
||||
"filter[inserted_at__lte]": date_range[1],
|
||||
@@ -186,39 +185,21 @@ class FindingsTools(BaseTool):
|
||||
) -> dict[str, Any]:
|
||||
"""Retrieve comprehensive details about a specific security finding by its ID.
|
||||
|
||||
IMPORTANT: This tool returns COMPLETE finding details.
|
||||
Use this after finding a specific finding via prowler_app_search_security_findings
|
||||
This tool provides MORE detailed information than search_security_findings. Use this when you need
|
||||
to deeply analyze a specific finding or understand its complete context and history.
|
||||
|
||||
This tool provides ALL information that prowler_app_search_security_findings returns PLUS:
|
||||
|
||||
1. Check Metadata (information about the check script that generated the finding):
|
||||
- title: Human-readable phrase used to summarize the check
|
||||
- description: Detailed explanation of what the check validates and why it is important
|
||||
- risk: What could happen if this check fails
|
||||
- remediation: Complete remediation guidance including step-by-step instructions and code snippets with best practices to fix the issue:
|
||||
* cli: Command-line commands to fix the issue
|
||||
* terraform: Terraform code snippets with best practices
|
||||
* nativeiac: Provider native Infrastructure as Code code snippets with best practices to fix the issue
|
||||
* other: Other remediation code snippets with best practices, usually used for web interfaces or other tools
|
||||
* recommendation: Text description with general best recommended practices to avoid the issue
|
||||
- provider: Cloud provider (aws/azure/gcp/etc)
|
||||
- service: Service name (s3/ec2/keyvault/etc)
|
||||
- resource_type: Resource type being evaluated
|
||||
- categories: Security categories this check belongs to
|
||||
- additional_urls: List of additional URLs related to the check
|
||||
|
||||
2. Temporal Metadata:
|
||||
- inserted_at: When this finding was first inserted into database
|
||||
- updated_at: When this finding was last updated
|
||||
- first_seen_at: When this finding was first detected across all scans
|
||||
|
||||
3. Relationships:
|
||||
- scan_id: UUID of the scan that generated this finding
|
||||
- resource_ids: List of UUIDs for cloud resources associated with this finding
|
||||
Additional information compared to search_security_findings:
|
||||
- Temporal metadata: when the finding was first seen, inserted, and last updated
|
||||
- Scan relationship: ID of the scan that generated this finding
|
||||
- Resource relationships: IDs of all cloud resources associated with this finding
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_search_security_findings to browse and filter findings
|
||||
2. Use this tool with the finding 'id' to get remediation guidance and complete context
|
||||
1. Use search_security_findings to browse and filter across many findings
|
||||
2. Use get_finding_details to drill down into specific findings of interest
|
||||
|
||||
Returns:
|
||||
dict containing detailed finding with comprehensive security metadata, temporal information,
|
||||
and relationships to scans and resources
|
||||
"""
|
||||
params = {
|
||||
# Return comprehensive fields including temporal metadata
|
||||
@@ -229,7 +210,7 @@ class FindingsTools(BaseTool):
|
||||
|
||||
# Get API response and transform to detailed format
|
||||
api_response = await self.api_client.get(
|
||||
f"/findings/{finding_id}", params=params
|
||||
f"/api/v1/findings/{finding_id}", params=params
|
||||
)
|
||||
detailed_finding = DetailedFinding.from_api_response(
|
||||
api_response.get("data", {})
|
||||
@@ -244,31 +225,26 @@ class FindingsTools(BaseTool):
|
||||
description="Filter statistics by cloud provider. Multiple values allowed. If empty, all providers are returned. For valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Get aggregate statistics and trends about security findings as a markdown report.
|
||||
"""Get high-level statistics about security findings formatted as a human-readable markdown report.
|
||||
|
||||
This tool provides a HIGH-LEVEL OVERVIEW without retrieving individual findings. Use this when you
|
||||
need to understand the overall security posture, trends, or remediation progress across all findings.
|
||||
Use this tool to get a quick overview of your security posture without retrieving individual findings.
|
||||
Perfect for understanding trends, identifying areas of concern, and tracking improvements over time.
|
||||
|
||||
The markdown report includes:
|
||||
The report includes:
|
||||
- Summary statistics: total findings, fail/pass/muted counts with percentages
|
||||
- Delta analysis: breakdown of new vs changed findings
|
||||
- Trending information: how findings are evolving over time
|
||||
|
||||
1. Summary Statistics:
|
||||
- Total number of findings
|
||||
- Failed checks (security issues) with percentage
|
||||
- Passed checks (no issues) with percentage
|
||||
- Muted findings (user-suppressed) with percentage
|
||||
Output format: Markdown-formatted report ready to present to users or include in documentation.
|
||||
|
||||
2. Delta Analysis (Change Tracking):
|
||||
- New findings: never seen before in previous scans
|
||||
* Broken down by: new failures, new passes, new muted
|
||||
- Changed findings: status changed since last scan
|
||||
* Broken down by: changed to fail, changed to pass, changed to muted
|
||||
- Unchanged findings: same status as previous scan
|
||||
Use cases:
|
||||
- Quick security posture assessment
|
||||
- Tracking remediation progress over time
|
||||
- Identifying which providers have most issues
|
||||
- Understanding finding trends (improving or degrading)
|
||||
|
||||
This helps answer questions like:
|
||||
- "What's my overall security posture?"
|
||||
- "How many critical security issues do I have?"
|
||||
- "Are we improving or getting worse over time?"
|
||||
- "How many new security issues appeared since last scan?"
|
||||
Returns:
|
||||
Dictionary with 'report' key containing markdown-formatted summary statistics
|
||||
"""
|
||||
params = {
|
||||
# Return only LLM-relevant aggregate statistics
|
||||
@@ -282,7 +258,7 @@ class FindingsTools(BaseTool):
|
||||
|
||||
# Get API response and transform to simplified format
|
||||
api_response = await self.api_client.get(
|
||||
"/overviews/findings", params=clean_params
|
||||
"/api/v1/overviews/findings", params=clean_params
|
||||
)
|
||||
overview = FindingsOverview.from_api_response(api_response)
|
||||
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
"""Muting tools for Prowler App MCP Server.
|
||||
|
||||
This module provides tools for managing finding muting in Prowler, including:
|
||||
- Mutelist management (pattern-based bulk muting)
|
||||
- Mute rules management (finding-specific muting)
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class MutingTools(BaseTool):
|
||||
"""Tools for muting operations.
|
||||
|
||||
Provides tools for:
|
||||
- Managing mutelist (pattern-based bulk muting)
|
||||
- Managing mute rules (finding-specific muting)
|
||||
"""
|
||||
|
||||
# ===== MUTELIST TOOLS =====
|
||||
|
||||
async def get_mutelist(self) -> dict[str, Any]:
|
||||
"""Retrieve the current mutelist configuration for the tenant.
|
||||
|
||||
IMPORTANT: Only one mutelist can exist per tenant. Returns an error message if no mutelist exists.
|
||||
For detailed information about mutelist structure and configuration, search Prowler documentation
|
||||
using prowler_docs_search tool available in this MCP Server.
|
||||
|
||||
The mutelist includes:
|
||||
- Core identification: id (UUID for processor operations)
|
||||
- Configuration: Nested structure with Accounts → Checks → Regions/Resources/Tags/Exceptions patterns
|
||||
- Temporal data: inserted_at, updated_at timestamps
|
||||
|
||||
Workflow:
|
||||
1. Use this tool to check if a mutelist is configured
|
||||
2. Examine current muting patterns before making updates
|
||||
3. Use prowler_app_set_mutelist to create or update the configuration
|
||||
"""
|
||||
self.logger.info("Retrieving mutelist configuration...")
|
||||
|
||||
# Query processors filtered by type=mutelist
|
||||
params = {
|
||||
"filter[processor_type]": "mutelist",
|
||||
"fields[processors]": "processor_type,configuration,inserted_at,updated_at",
|
||||
}
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
api_response = await self.api_client.get("/processors", params=clean_params)
|
||||
|
||||
data = api_response.get("data", [])
|
||||
|
||||
if len(data) == 0:
|
||||
return {
|
||||
"error": "No mutelist found",
|
||||
"message": "No mutelist configuration exists for this tenant. Use prowler_app_set_mutelist to create one.",
|
||||
}
|
||||
|
||||
# Return the first (and only) mutelist
|
||||
mutelist = MutelistResponse.from_api_response(data[0])
|
||||
return mutelist.model_dump()
|
||||
|
||||
async def set_mutelist(
|
||||
self,
|
||||
configuration: dict[str, Any] | str = Field(
|
||||
description="""Mutelist configuration object following the Accounts/Checks/Regions/Resources/Tags/Exceptions structure.
|
||||
Accepts either a dictionary or JSON string. The configuration replaces the entire mutelist (not merged with existing).
|
||||
|
||||
Structure:
|
||||
{
|
||||
"Mutelist": {
|
||||
"Accounts": {
|
||||
"<account-pattern>": { // "*" for all accounts, or specific account ID
|
||||
"Checks": {
|
||||
"<check-id>": { // Prowler check ID
|
||||
"Regions": ["us-east-1", "eu-west-1"], // Optional
|
||||
"Resources": ["arn:aws:s3:::my-bucket"], // Optional
|
||||
"Tags": ["Environment:dev"], // Optional
|
||||
"Exceptions": { // Optional
|
||||
"Accounts": ["123456789012"],
|
||||
"Regions": ["us-west-2"],
|
||||
"Resources": ["arn:aws:s3:::critical-bucket"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Create or update the mutelist configuration for pattern-based bulk muting.
|
||||
|
||||
IMPORTANT: Automatically creates a new mutelist or updates the existing one (only one mutelist per tenant).
|
||||
The configuration completely replaces any existing mutelist (not merged).
|
||||
For detailed information about mutelist structure and configuration, search Prowler documentation
|
||||
using prowler_docs_search tool available in this MCP Server.
|
||||
|
||||
Default behavior:
|
||||
- Creates new mutelist if none exists
|
||||
- Updates existing mutelist with complete replacement
|
||||
- Applies to findings from future scans
|
||||
|
||||
The mutelist supports:
|
||||
- Account patterns: Specific account IDs or "*" for all
|
||||
- Check-based muting: Per-check ID configuration
|
||||
- Scope filtering: Regions, Resources, Tags
|
||||
- Exceptions: Accounts, Regions, Resources to exclude from muting
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_get_mutelist to check existing configuration
|
||||
2. Build configuration object following Prowler mutelist format
|
||||
3. Use this tool to create or update the mutelist
|
||||
4. Verify with prowler_app_get_mutelist
|
||||
"""
|
||||
self.logger.info("Setting mutelist configuration...")
|
||||
|
||||
# Parse configuration if it's a string
|
||||
if isinstance(configuration, str):
|
||||
configuration = json.loads(configuration)
|
||||
|
||||
# Check if mutelist already exists
|
||||
existing_mutelist = await self.get_mutelist()
|
||||
|
||||
if "error" in existing_mutelist:
|
||||
# Create new mutelist
|
||||
self.logger.info("Creating new mutelist...")
|
||||
create_body = {
|
||||
"data": {
|
||||
"type": "processors",
|
||||
"attributes": {
|
||||
"processor_type": "mutelist",
|
||||
"configuration": configuration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
api_response = await self.api_client.post(
|
||||
"/processors", json_data=create_body
|
||||
)
|
||||
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
|
||||
return mutelist.model_dump()
|
||||
else:
|
||||
# Update existing mutelist
|
||||
self.logger.info(f"Updating existing mutelist {existing_mutelist['id']}...")
|
||||
update_body = {
|
||||
"data": {
|
||||
"type": "processors",
|
||||
"id": existing_mutelist["id"],
|
||||
"attributes": {
|
||||
"configuration": configuration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
api_response = await self.api_client.patch(
|
||||
f"/processors/{existing_mutelist['id']}", json_data=update_body
|
||||
)
|
||||
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
|
||||
return mutelist.model_dump()
|
||||
|
||||
async def delete_mutelist(self) -> dict[str, Any]:
|
||||
"""Remove the mutelist configuration from the tenant.
|
||||
|
||||
WARNING: This is a destructive operation that cannot be undone.
|
||||
- The mutelist will need to be re-created with prowler_app_set_mutelist
|
||||
- New findings from future scans will NOT be muted by the deleted mutelist
|
||||
- Previously muted findings remain muted (deletion doesn't un-mute them)
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_get_mutelist to confirm what will be deleted
|
||||
2. Use this tool to permanently remove the mutelist
|
||||
3. New scans will no longer apply mutelist-based muting
|
||||
"""
|
||||
self.logger.info("Deleting mutelist configuration...")
|
||||
|
||||
# Get existing mutelist
|
||||
existing_mutelist = await self.get_mutelist()
|
||||
|
||||
if "error" in existing_mutelist:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No mutelist found to delete",
|
||||
}
|
||||
|
||||
# Delete the mutelist
|
||||
mutelist_id = existing_mutelist["id"]
|
||||
await self.api_client.delete(f"/processors/{mutelist_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Mutelist deleted successfully",
|
||||
}
|
||||
|
||||
# ===== MUTE RULES TOOLS =====
|
||||
|
||||
async def list_mute_rules(
|
||||
self,
|
||||
name: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by exact rule name",
|
||||
),
|
||||
enabled: (
|
||||
bool | str | None
|
||||
) = Field( # Wrong `str` hint type due to bad MCP Clients implementation
|
||||
default=None,
|
||||
description="Filter by enabled status. True for enabled rules only, False for disabled rules only. If not specified, returns both enabled and disabled rules. Strings 'true' and 'false' are also accepted.",
|
||||
),
|
||||
search: str | None = Field(
|
||||
default=None,
|
||||
description="Free-text search term across multiple fields (name, reason). Use this for general keyword search.",
|
||||
),
|
||||
page_size: int = Field(
|
||||
default=50, description="Number of results to return per page."
|
||||
),
|
||||
page_number: int = Field(
|
||||
default=1,
|
||||
description="Page number to retrieve (1-indexed)",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Search and filter mute rules with pagination support.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT mute rules without the full list of finding UIDs.
|
||||
Use prowler_app_get_mute_rule to get complete details including all finding UIDs and creator information.
|
||||
|
||||
Default behavior:
|
||||
- Returns all mute rules (both enabled and disabled)
|
||||
- Returns 50 rules per page
|
||||
- Includes basic rule information without full finding UID lists
|
||||
|
||||
Each mute rule includes:
|
||||
- Core identification: id (UUID for prowler_app_get_mute_rule), name
|
||||
- Contextual information: reason, enabled status
|
||||
- State tracking: finding_count (number of findings currently muted)
|
||||
- Temporal data: inserted_at, updated_at timestamps
|
||||
|
||||
Workflow:
|
||||
1. Use this tool to search and filter mute rules by name, enabled status, or keywords
|
||||
2. Use prowler_app_get_mute_rule with the mute rule 'id' to get complete details including all finding UIDs
|
||||
3. Use prowler_app_update_mute_rule or prowler_app_delete_mute_rule to modify rules
|
||||
"""
|
||||
self.logger.info("Listing mute rules...")
|
||||
self.api_client.validate_page_size(page_size)
|
||||
|
||||
params = {
|
||||
"fields[mute-rules]": "name,reason,enabled,finding_uids,inserted_at,updated_at",
|
||||
"page[size]": page_size,
|
||||
"page[number]": page_number,
|
||||
}
|
||||
|
||||
# Build filter parameters
|
||||
if name:
|
||||
params["filter[name]"] = name
|
||||
if enabled is not None:
|
||||
if isinstance(enabled, bool):
|
||||
params["filter[enabled]"] = enabled
|
||||
else:
|
||||
if enabled.lower() == "true":
|
||||
params["filter[enabled]"] = True
|
||||
elif enabled.lower() == "false":
|
||||
params["filter[enabled]"] = False
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid enabled value: {enabled}. Valid values are True, False, 'true', 'false' or None."
|
||||
)
|
||||
if search:
|
||||
params["filter[search]"] = search
|
||||
|
||||
clean_params = self.api_client.build_filter_params(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()
|
||||
|
||||
async def get_mute_rule(
|
||||
self,
|
||||
rule_id: str = Field(
|
||||
description="UUID of the mute rule to retrieve. Must be a valid UUID format (e.g., '019ac0d6-90d5-73e9-9acf-c22e256f1bac')."
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Retrieve comprehensive details about a specific mute rule by its ID.
|
||||
|
||||
IMPORTANT: This tool returns COMPLETE mute rule details including the full list of finding UIDs.
|
||||
Use this after finding a rule via prowler_app_list_mute_rules.
|
||||
|
||||
This tool provides ALL information that prowler_app_list_mute_rules returns PLUS:
|
||||
- finding_uids: Complete list of finding UIDs that are muted by this rule
|
||||
- user_creator_id: UUID of the user who created the rule (audit trail)
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_list_mute_rules to find rules by name or filter criteria
|
||||
2. Use this tool with the rule 'id' to get complete details
|
||||
3. Examine finding_uids list to understand which findings are muted
|
||||
4. Use prowler_app_update_mute_rule or prowler_app_delete_mute_rule to modify if needed
|
||||
"""
|
||||
self.logger.info(f"Retrieving mute rule {rule_id}...")
|
||||
|
||||
params = {
|
||||
"include": "created_by",
|
||||
}
|
||||
|
||||
api_response = await self.api_client.get(
|
||||
f"/mute-rules/{rule_id}", params=params
|
||||
)
|
||||
|
||||
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
|
||||
return detailed_rule.model_dump()
|
||||
|
||||
async def create_mute_rule(
|
||||
self,
|
||||
name: str = Field(
|
||||
description="Name for the mute rule. Should be descriptive and meaningful (e.g., 'Dev S3 Public Access', 'Test Environment IMDSv1')."
|
||||
),
|
||||
reason: str = Field(
|
||||
description="Reason for muting these findings. Document why this security issue is acceptable or intentional (e.g., 'Development environment with controlled access', 'Legacy application requires IMDSv1')."
|
||||
),
|
||||
finding_ids: list[str] = Field(
|
||||
description="List of finding IDs (UUIDs) to mute. Get these from the prowler_app_search_security_findings tool. Must provide at least 1 finding ID."
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new mute rule to mute specific findings with documentation and audit trail.
|
||||
|
||||
IMPORTANT: This immediately mutes the specified findings AND all previous findings with matching UIDs (this could take some time to complete).
|
||||
The rule is enabled by default. Muting is permanent.
|
||||
|
||||
Default behavior:
|
||||
- Rule is created in enabled state
|
||||
- Applies to current and previous findings with matching UIDs
|
||||
- Records creator for audit trail
|
||||
|
||||
The mute rule includes:
|
||||
- Core identification: id (UUID for prowler_app_get_mute_rule), name, reason
|
||||
- Configuration: enabled status, finding_uids list
|
||||
- Audit trail: user_creator_id (UUID of the Prowler user from the tenant that created the rule), timestamps when the rule was created and last modified
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_search_security_findings to identify findings to mute
|
||||
2. Use this tool with finding IDs, descriptive name, and documented reason
|
||||
3. Verify with prowler_app_get_mute_rule to confirm rule creation
|
||||
4. Check findings are muted with prowler_app_search_security_findings (filter by muted=true)
|
||||
"""
|
||||
self.logger.info(f"Creating mute rule '{name}'...")
|
||||
|
||||
create_body = {
|
||||
"data": {
|
||||
"type": "mute-rules",
|
||||
"attributes": {
|
||||
"name": name,
|
||||
"reason": reason,
|
||||
"finding_ids": finding_ids,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
async def update_mute_rule(
|
||||
self,
|
||||
rule_id: str = Field(
|
||||
description="UUID of the mute rule to update. Must be a valid UUID format."
|
||||
),
|
||||
name: str | None = Field(
|
||||
default=None,
|
||||
description="New name for the rule. If not specified, name remains unchanged.",
|
||||
),
|
||||
reason: str | None = Field(
|
||||
default=None,
|
||||
description="New reason for the rule. If not specified, reason remains unchanged.",
|
||||
),
|
||||
enabled: bool | None = Field(
|
||||
default=None,
|
||||
description="Enable (True) or disable (False) the rule. If not specified, enabled status remains unchanged. IMPORTANT: Disabling a rule does not un-mute findings - they remain muted.",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Update a mute rule's name, reason, or enabled status.
|
||||
|
||||
IMPORTANT: Cannot change which findings are muted (finding_uids are immutable).
|
||||
Disabling a rule does NOT un-mute findings - they remain muted permanently.
|
||||
|
||||
Default behavior:
|
||||
- Only specified fields are updated
|
||||
- Unspecified fields remain unchanged
|
||||
- If no parameters provided, returns current rule state
|
||||
|
||||
Updatable fields:
|
||||
- name: Change rule name for better organization
|
||||
- reason: Update documentation/justification
|
||||
- enabled: Toggle rule active status (doesn't affect already-muted findings)
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_get_mute_rule to see current rule state
|
||||
2. Use this tool to update name, reason, or enabled status
|
||||
3. Verify changes with prowler_app_get_mute_rule
|
||||
"""
|
||||
self.logger.info(f"Updating mute rule {rule_id}...")
|
||||
|
||||
# Build update body with only provided fields
|
||||
attributes = {}
|
||||
if name is not None:
|
||||
attributes["name"] = name
|
||||
if reason is not None:
|
||||
attributes["reason"] = reason
|
||||
if enabled is not None:
|
||||
attributes["enabled"] = enabled
|
||||
|
||||
if not attributes:
|
||||
# No updates provided, just return current state
|
||||
return await self.get_mute_rule(rule_id)
|
||||
|
||||
update_body = {
|
||||
"data": {
|
||||
"type": "mute-rules",
|
||||
"id": rule_id,
|
||||
"attributes": attributes,
|
||||
}
|
||||
}
|
||||
|
||||
api_response = await self.api_client.patch(
|
||||
f"/mute-rules/{rule_id}", json_data=update_body
|
||||
)
|
||||
|
||||
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
|
||||
return detailed_rule.model_dump()
|
||||
|
||||
async def delete_mute_rule(
|
||||
self,
|
||||
rule_id: str = Field(
|
||||
description="UUID of the mute rule to delete. Must be a valid UUID format."
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Delete a mute rule from the system.
|
||||
|
||||
WARNING: Findings that were muted by this rule REMAIN MUTED after deletion.
|
||||
This only removes the rule itself from management, not the muting effect on findings.
|
||||
The muted findings will stay muted permanently.
|
||||
|
||||
Deletion behavior:
|
||||
- Rule is permanently removed from the system
|
||||
- Muted findings remain muted (deletion doesn't un-mute them)
|
||||
- Cannot be undone - rule must be recreated to restore
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_get_mute_rule to review what will be deleted
|
||||
2. Use this tool to permanently remove the rule
|
||||
3. Verify deletion with prowler_app_list_mute_rules (rule should no longer appear)
|
||||
"""
|
||||
self.logger.info(f"Deleting mute rule {rule_id}...")
|
||||
|
||||
result = await self.api_client.delete(f"/mute-rules/{rule_id}")
|
||||
|
||||
if result.get("success"):
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Mute rule deleted successfully",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to delete mute rule",
|
||||
}
|
||||
@@ -1,620 +0,0 @@
|
||||
"""Provider Management tools for Prowler App MCP Server.
|
||||
|
||||
This module provides tools for managing provider connections,
|
||||
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
|
||||
|
||||
|
||||
class ProvidersTools(BaseTool):
|
||||
"""Tools for provider management operations
|
||||
|
||||
Provides tools for:
|
||||
- prowler_app_search_providers: Search and view configured providers with their connection status
|
||||
- prowler_app_connect_provider: Connect or register a provider for security scanning in Prowler
|
||||
- prowler_app_delete_provider: Permanently remove a provider from Prowler
|
||||
"""
|
||||
|
||||
async def search_providers(
|
||||
self,
|
||||
provider_id: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by Prowler's internal UUID(s) (v4) for the provider(s), generated when the provider is registered in the system.",
|
||||
),
|
||||
provider_uid: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by provider's unique identifier(s), this ID is the one provided by the provider itself. Format varies by provider type: AWS Account ID (12 digits), Azure Subscription ID (UUID), GCP Project ID (string), Kubernetes namespace, GitHub username/organization, M365 domain ID, etc. All supported provider types are listed in the Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
|
||||
),
|
||||
provider_type: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by provider type. Valid values include: 'aws', 'azure', 'gcp', 'kubernetes'... For more valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
|
||||
),
|
||||
alias: str | None = Field(
|
||||
default=None,
|
||||
description="Search by provider alias/friendly name. Partial match supported (case-insensitive). Use this to find providers by their human-readable name (e.g., 'Production', 'Dev', 'AWS Main')",
|
||||
),
|
||||
connected: (
|
||||
bool | str | None
|
||||
) = Field( # Wrong `str` hint type due to bad MCP Clients implementation
|
||||
default=None,
|
||||
description="Filter by connection status. True returns only successfully connected providers (credentials work), False returns only providers with failed connections (credentials invalid). If not specified, returns all connected, failed and not tested providers. Strings 'true' and 'false' are also accepted.",
|
||||
),
|
||||
page_size: int = Field(
|
||||
default=50, description="Number of results to return per page"
|
||||
),
|
||||
page_number: int = Field(
|
||||
default=1,
|
||||
description="Page number to retrieve (1-indexed)",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Search and view configured providers to be scanned with Prowler.
|
||||
|
||||
This tool returns a unified view of all providers configured in Prowler.
|
||||
|
||||
For getting more details about what types of providers are available to be scanned with Prowler or
|
||||
what are the UIDs are accepted for each provider type, please refer to Prowler Hub/Prowler Documentation
|
||||
that you can also find in form of tools in this MCP Server.
|
||||
|
||||
Each provider includes:
|
||||
- Provider identification: Prowler Internal ID, External Provider UID, Provider Alias
|
||||
- Provider context: Provider Type
|
||||
- Connection status: Connected (true), Failed (false), Not Tested (null)
|
||||
"""
|
||||
self.api_client.validate_page_size(page_size)
|
||||
|
||||
params = {
|
||||
"fields[providers]": "uid,alias,provider,connection,secret",
|
||||
"page[number]": page_number,
|
||||
"page[size]": page_size,
|
||||
}
|
||||
|
||||
# Build filter parameters
|
||||
if provider_id:
|
||||
params["filter[id__in]"] = provider_id
|
||||
if provider_uid:
|
||||
params["filter[uid__in]"] = provider_uid
|
||||
if provider_type:
|
||||
params["filter[provider__in]"] = provider_type
|
||||
if alias:
|
||||
params["filter[alias__icontains]"] = alias
|
||||
if connected is not None:
|
||||
if isinstance(connected, bool):
|
||||
params["filter[connected]"] = connected
|
||||
else:
|
||||
if connected.lower() == "true":
|
||||
params["filter[connected]"] = True
|
||||
elif connected.lower() == "false":
|
||||
params["filter[connected]"] = False
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid connected value: {connected}. Valid values are True, False, 'true', 'false' or None."
|
||||
)
|
||||
|
||||
clean_params = self.api_client.build_filter_params(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
|
||||
for provider in simplified_response.providers:
|
||||
# Get the provider data from the API response to access relationships
|
||||
provider_data = next(
|
||||
(
|
||||
provider_api_response
|
||||
for provider_api_response in api_response["data"]
|
||||
if provider_api_response["id"] == provider.id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if provider_data:
|
||||
secret_relationship = provider_data.get("relationships", {}).get(
|
||||
"secret", {}
|
||||
)
|
||||
secret_data = secret_relationship.get("data")
|
||||
if secret_data:
|
||||
secret_id = secret_data["id"]
|
||||
provider.secret_type = await self._get_secret_type(secret_id)
|
||||
|
||||
return simplified_response.model_dump()
|
||||
|
||||
async def connect_provider(
|
||||
self,
|
||||
provider_uid: str = Field(
|
||||
description="Provider's unique identifier. For supported UID provider formats, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server"
|
||||
),
|
||||
provider_type: str = Field(
|
||||
description="Type of provider to be scanned with Prowler. Valid values include: 'aws', 'azure', 'gcp', 'kubernetes'... For more valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server."
|
||||
),
|
||||
alias: str | None = Field(
|
||||
default=None,
|
||||
description="Human-friendly name for this provider. Optional but recommended for easy identification. Use descriptive names to distinguish multiple accounts of the same type.",
|
||||
),
|
||||
credentials: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Provider-specific credentials for authentication. Optional - if not provided, provider is created but not connected. Structure varies by provider type. For supported provider types, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Register a provider to be scanned with Prowler.
|
||||
|
||||
This tool will register a provider in Prowler App, even if the UID is wrong.
|
||||
If the provider is already registered, it will be updated with the new provided alias or credentials if provided.
|
||||
If credentials are provided, they will be added to the indicated provider, if the provider does not exist, it will be created and the credentials will be added to it.
|
||||
If the connection test is successful, the provider will be connected.
|
||||
If the connection test fails, the provider will be created but not connected.
|
||||
The tool always returns the provider details after its registration or update.
|
||||
|
||||
Example Input:
|
||||
- AWS Static Credentials:
|
||||
```json
|
||||
{
|
||||
"provider_uid": "123456789012",
|
||||
"provider_type": "aws",
|
||||
"alias": "production-aws-account",
|
||||
"credentials": {
|
||||
"aws_access_key_id": "AKIA...",
|
||||
"aws_secret_access_key": "...",
|
||||
"aws_session_token": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
- AWS Assume Role:
|
||||
```json
|
||||
{
|
||||
"provider_uid": "987654321098",
|
||||
"provider_type": "aws",
|
||||
"alias": "staging-aws-account",
|
||||
"credentials": {
|
||||
"role_arn": "arn:aws:iam::987654321098:role/ProwlerScanRole",
|
||||
"external_id": "...",
|
||||
"aws_access_key_id": "AKIA...", # Optional
|
||||
"aws_secret_access_key": "...", # Optional
|
||||
"aws_session_token": "...", # Optional
|
||||
"session_duration": 3600, # Optional
|
||||
"role_session_name": "..." # Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
- Azure/M365 Static Credentials:
|
||||
```json
|
||||
{
|
||||
"provider_uid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
|
||||
"provider_type": "azure",
|
||||
"alias": "production-azure-subscription",
|
||||
"credentials": {
|
||||
"client_id": "...",
|
||||
"client_secret": "...",
|
||||
"tenant_id": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
- GCP Service Account Account Key:
|
||||
```json
|
||||
{
|
||||
"provider_uid": "my-gcp-project-prod",
|
||||
"provider_type": "gcp",
|
||||
"alias": "production-gcp-project",
|
||||
"credentials": {
|
||||
"service_account_key": {
|
||||
"type": "service_account",
|
||||
"project_id": "...",
|
||||
"private_key_id": "...",
|
||||
"private_key": "...",
|
||||
"client_email": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- Kubernetes Static Credentials:
|
||||
```json
|
||||
{
|
||||
"provider_uid": "prod-k8s-cluster",
|
||||
"provider_type": "kubernetes",
|
||||
"alias": "production-kubernetes-cluster",
|
||||
"credentials": {
|
||||
"kubeconfig_content": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
- GitHub OAuth App Token:
|
||||
```json
|
||||
{
|
||||
"provider_uid": "my-organization",
|
||||
"provider_type": "github",
|
||||
"alias": "my-github-organization",
|
||||
"credentials": {
|
||||
"oauth_app_token": "..."
|
||||
}
|
||||
}
|
||||
|
||||
NOTE: THERE ARE MORE PROVIDER TYPES AND CREDENTIAL TYPES AVAILABLE, PLEASE REFER TO THE Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.
|
||||
"""
|
||||
# Step 1: Check if provider already exists
|
||||
prowler_provider_id = await self._check_provider_exists(provider_uid)
|
||||
|
||||
# Step 2: Create or update provider
|
||||
if prowler_provider_id is None:
|
||||
prowler_provider_id = await self._create_provider(
|
||||
provider_uid, provider_type, alias
|
||||
)
|
||||
elif alias:
|
||||
await self._update_provider_alias(prowler_provider_id, alias)
|
||||
|
||||
# Step 3: Handle credentials if provided and capture secret response
|
||||
secret_response = None
|
||||
if credentials:
|
||||
secret_response = await self._store_credentials(
|
||||
prowler_provider_id, credentials
|
||||
)
|
||||
|
||||
# Step 4: Test connection
|
||||
connection_status = await self._test_connection(prowler_provider_id)
|
||||
|
||||
# Step 5: Get final provider state with relationships
|
||||
final_provider = await self._get_final_provider_state(prowler_provider_id)
|
||||
|
||||
# Transform to structured response using model
|
||||
connection_result = ProviderConnectionStatus.create(
|
||||
provider_data=final_provider["data"],
|
||||
connection_status=connection_status,
|
||||
)
|
||||
|
||||
if secret_response:
|
||||
# We just stored credentials, use the secret_type from the response
|
||||
connection_result.provider.secret_type = (
|
||||
secret_response.get("data", {}).get("attributes", {}).get("secret_type")
|
||||
)
|
||||
else:
|
||||
# No new credentials provided, check if provider has an existing secret
|
||||
secret_data = (
|
||||
final_provider.get("data", {})
|
||||
.get("relationships", {})
|
||||
.get("secret", {})
|
||||
.get("data")
|
||||
)
|
||||
if secret_data:
|
||||
# Provider has existing secret, fetch its type
|
||||
secret_id = secret_data["id"]
|
||||
connection_result.provider.secret_type = await self._get_secret_type(
|
||||
secret_id
|
||||
)
|
||||
|
||||
return connection_result.model_dump()
|
||||
|
||||
async def delete_provider(
|
||||
self,
|
||||
provider_id: str = Field(
|
||||
description="Prowler's internal UUID (v4) for the provider to permanently remove, generated when the provider was registered in the system. Use `prowler_app_search_providers` tool to find the provider_id if you only know the alias or the provider's own identifier (provider_uid)"
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Permanently remove a registered provider from Prowler.
|
||||
|
||||
WARNING: This is a destructive operation that cannot be undone. The provider will need to be
|
||||
re-added with prowler_app_connect_provider if you want to scan it again.
|
||||
|
||||
The tool always returns the deletion status and message.
|
||||
"""
|
||||
self.logger.info(f"Deleting provider {provider_id}...")
|
||||
try:
|
||||
# Initiate the deletion task
|
||||
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)
|
||||
await self.api_client.poll_task_until_complete(
|
||||
task_id=task_id, timeout=60, poll_interval=1.0
|
||||
)
|
||||
|
||||
# If we reach here, the task completed successfully
|
||||
return {
|
||||
"deleted": True,
|
||||
"message": f"Provider {provider_id} deleted successfully",
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Provider deletion failed: {e}")
|
||||
return {
|
||||
"deleted": False,
|
||||
"message": f"Provider {provider_id} deletion failed: {str(e)}",
|
||||
}
|
||||
|
||||
# Private helper methods
|
||||
|
||||
async def _check_provider_exists(self, provider_uid: str) -> str | None:
|
||||
"""Check if a provider already exists by its UID.
|
||||
|
||||
Args:
|
||||
provider_uid: The provider's unique identifier (e.g., AWS account ID)
|
||||
|
||||
Returns:
|
||||
The Prowler-generated provider ID if exists, None otherwise
|
||||
|
||||
Raises:
|
||||
Exception: If multiple providers with the same UID are found (data integrity issue)
|
||||
Exception: If API request fails
|
||||
"""
|
||||
self.logger.info(f"Checking if provider {provider_uid} exists...")
|
||||
response = await self.api_client.get(
|
||||
"/providers", params={"filter[uid]": provider_uid}
|
||||
)
|
||||
providers = response.get("data", [])
|
||||
|
||||
if len(providers) == 0:
|
||||
self.logger.info(f"Provider {provider_uid} does not exist")
|
||||
return None
|
||||
elif len(providers) == 1:
|
||||
prowler_provider_id = providers[0].get("id")
|
||||
self.logger.info(
|
||||
f"Provider {provider_uid} exists with ID {prowler_provider_id}"
|
||||
)
|
||||
return prowler_provider_id
|
||||
else:
|
||||
# Multiple providers with the same UID is a data integrity issue
|
||||
raise Exception(
|
||||
f"Data integrity error: Found {len(providers)} providers with UID '{provider_uid}'. "
|
||||
f"Each provider UID should be unique. Please contact support or manually clean up duplicate providers."
|
||||
)
|
||||
|
||||
async def _create_provider(
|
||||
self, provider_uid: str, provider_type: str, alias: str | None
|
||||
) -> str:
|
||||
"""Create a new provider.
|
||||
|
||||
Args:
|
||||
provider_uid: The provider's unique identifier
|
||||
provider_type: Type of provider to be scanned with Prowler (aws, azure, gcp, etc.)
|
||||
alias: Optional human-friendly name for the provider
|
||||
|
||||
Returns:
|
||||
The provider UID (which is used as the ID)
|
||||
"""
|
||||
self.logger.info(f"Creating provider {provider_uid} (type: {provider_type})...")
|
||||
provider_body = {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"uid": provider_uid,
|
||||
"provider": provider_type,
|
||||
},
|
||||
}
|
||||
}
|
||||
if alias:
|
||||
provider_body["data"]["attributes"]["alias"] = alias
|
||||
|
||||
await self.api_client.post("/providers", json_data=provider_body)
|
||||
|
||||
provider_id = await self._check_provider_exists(provider_uid)
|
||||
if provider_id is None:
|
||||
raise Exception(f"Provider {provider_uid} creation failed")
|
||||
return provider_id
|
||||
|
||||
async def _update_provider_alias(
|
||||
self, prowler_provider_id: str, alias: str
|
||||
) -> None:
|
||||
"""Update the alias of an existing provider.
|
||||
|
||||
Args:
|
||||
prowler_provider_id: The Prowler-generated provider ID
|
||||
alias: New human-friendly name for the provider
|
||||
"""
|
||||
self.logger.info(f"Updating provider {prowler_provider_id} alias...")
|
||||
update_body = {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"id": prowler_provider_id,
|
||||
"attributes": {
|
||||
"alias": alias,
|
||||
},
|
||||
}
|
||||
}
|
||||
result = await self.api_client.patch(
|
||||
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")
|
||||
|
||||
def _determine_secret_type(self, credentials: dict[str, Any]) -> str:
|
||||
"""Determine the secret type from credentials structure.
|
||||
|
||||
Args:
|
||||
credentials: The credentials dictionary
|
||||
|
||||
Returns:
|
||||
Secret type: "role", "service_account", or "static"
|
||||
"""
|
||||
if "role_arn" in credentials:
|
||||
return "role"
|
||||
elif "service_account_key" in credentials:
|
||||
return "service_account"
|
||||
else:
|
||||
return "static"
|
||||
|
||||
async def _get_provider_secret_id(self, prowler_provider_id: str) -> str | None:
|
||||
"""Get the secret ID for a provider if it exists.
|
||||
|
||||
Args:
|
||||
prowler_provider_id: The Prowler-generated provider ID
|
||||
|
||||
Returns:
|
||||
The secret ID if exists, None otherwise
|
||||
"""
|
||||
try:
|
||||
response = await self.api_client.get(
|
||||
"/providers/secrets",
|
||||
params={"filter[provider]": prowler_provider_id},
|
||||
)
|
||||
secrets = response.get("data", [])
|
||||
|
||||
if len(secrets) > 0:
|
||||
secret_id = secrets[0].get("id")
|
||||
self.logger.info(
|
||||
f"Found existing secret {secret_id} for provider {prowler_provider_id}"
|
||||
)
|
||||
return secret_id
|
||||
else:
|
||||
self.logger.info(
|
||||
f"No existing secret found for provider {prowler_provider_id}"
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking for existing secret: {e}")
|
||||
return None
|
||||
|
||||
async def _get_secret_type(self, secret_id: str) -> str | None:
|
||||
"""Get the secret type for a given secret ID.
|
||||
|
||||
Args:
|
||||
secret_id: The secret ID from provider relationships
|
||||
|
||||
Returns:
|
||||
The secret type ("role", "service_account", or "static") if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
response = await self.api_client.get(
|
||||
f"/providers/secrets/{secret_id}",
|
||||
params={"fields[provider-secrets]": "secret_type"},
|
||||
)
|
||||
secret_type = (
|
||||
response.get("data", {}).get("attributes", {}).get("secret_type")
|
||||
)
|
||||
return secret_type
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching secret type for {secret_id}: {e}")
|
||||
return None
|
||||
|
||||
async def _store_credentials(
|
||||
self, prowler_provider_id: str, credentials: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Store or update credentials for a provider.
|
||||
|
||||
Args:
|
||||
prowler_provider_id: The Prowler-generated provider ID
|
||||
credentials: The credentials to store
|
||||
|
||||
Returns:
|
||||
The API response with the secret data
|
||||
"""
|
||||
self.logger.info(
|
||||
f"Adding/updating credentials for provider {prowler_provider_id}..."
|
||||
)
|
||||
|
||||
secret_type = self._determine_secret_type(credentials)
|
||||
|
||||
# Check if a secret already exists for this provider
|
||||
existing_secret_id = await self._get_provider_secret_id(prowler_provider_id)
|
||||
|
||||
if existing_secret_id:
|
||||
# Update existing secret
|
||||
self.logger.info(f"Updating existing secret {existing_secret_id}...")
|
||||
update_body = {
|
||||
"data": {
|
||||
"type": "provider-secrets",
|
||||
"id": existing_secret_id,
|
||||
"attributes": {
|
||||
"secret_type": secret_type,
|
||||
"secret": credentials,
|
||||
},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"id": prowler_provider_id,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
try:
|
||||
response = await self.api_client.patch(
|
||||
f"/providers/secrets/{existing_secret_id}",
|
||||
json_data=update_body,
|
||||
)
|
||||
self.logger.info("Credentials updated successfully")
|
||||
return response
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating credentials: {e}")
|
||||
raise
|
||||
else:
|
||||
# Create new secret
|
||||
self.logger.info("Creating new secret...")
|
||||
secret_body = {
|
||||
"data": {
|
||||
"type": "provider-secrets",
|
||||
"attributes": {
|
||||
"secret_type": secret_type,
|
||||
"secret": credentials,
|
||||
},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"id": prowler_provider_id,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.api_client.post(
|
||||
"/providers/secrets", json_data=secret_body
|
||||
)
|
||||
self.logger.info("Credentials added successfully")
|
||||
return response
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error adding credentials: {e}")
|
||||
raise
|
||||
|
||||
async def _test_connection(self, prowler_provider_id: str) -> dict[str, Any]:
|
||||
"""Test connection to a provider.
|
||||
|
||||
Args:
|
||||
prowler_provider_id: The Prowler-generated provider ID
|
||||
|
||||
Returns:
|
||||
Connection status dictionary with 'connected' boolean and optional 'error' message
|
||||
"""
|
||||
self.logger.info(f"Testing connection for provider {prowler_provider_id}...")
|
||||
try:
|
||||
# Initiate the connection test task
|
||||
task_response = await self.api_client.post(
|
||||
f"/providers/{prowler_provider_id}/connection", json_data={}
|
||||
)
|
||||
task_id = task_response.get("data", {}).get("id")
|
||||
|
||||
# Poll until task completes (with 60 second timeout)
|
||||
completed_task = await self.api_client.poll_task_until_complete(
|
||||
task_id=task_id, timeout=60, poll_interval=1.0
|
||||
)
|
||||
|
||||
# Extract the result from the completed task
|
||||
task_result = (
|
||||
completed_task.get("data", {}).get("attributes", {}).get("result", {})
|
||||
)
|
||||
|
||||
return task_result
|
||||
except Exception as e:
|
||||
self.logger.error(f"Connection test failed: {e}")
|
||||
return {"connected": False, "error": str(e)}
|
||||
|
||||
async def _get_final_provider_state(
|
||||
self, prowler_provider_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""Get final provider state with relationships.
|
||||
|
||||
Args:
|
||||
prowler_provider_id: The Prowler-generated provider ID
|
||||
|
||||
Returns:
|
||||
Provider data dictionary
|
||||
"""
|
||||
return await self.api_client.get(
|
||||
f"/providers/{prowler_provider_id}",
|
||||
)
|
||||
@@ -1,345 +0,0 @@
|
||||
"""Cloud Resources tools for Prowler App MCP Server.
|
||||
|
||||
This module provides tools for searching, viewing, and analyzing cloud resources
|
||||
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
|
||||
|
||||
|
||||
class ResourcesTools(BaseTool):
|
||||
"""Tools for cloud resources operations.
|
||||
|
||||
Provides tools for:
|
||||
- Searching and filtering cloud resources
|
||||
- Getting detailed resource information
|
||||
- Viewing resources overview with statistics
|
||||
"""
|
||||
|
||||
async def list_resources(
|
||||
self,
|
||||
provider_type: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by provider type. Multiple values allowed. If empty, all providers are returned. For valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
|
||||
),
|
||||
provider_alias: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by specific provider alias/name (partial match supported). Useful for finding resources in specific accounts like 'production' or 'dev'.",
|
||||
),
|
||||
provider_uid: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by provider's native ID (e.g., AWS account ID, Azure subscription ID, GCP project ID). All supported provider types are listed in the Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
|
||||
),
|
||||
region: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by regions. Multiple values allowed (e.g., us-east-1, westus2, europe-west1), format may vary depending on the provider. If empty, all regions are returned.",
|
||||
),
|
||||
service: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by service. Multiple values allowed (e.g., s3, ec2, iam, keyvault). If empty, all services are returned.",
|
||||
),
|
||||
resource_type: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by resource type. Format may vary depending on the provider. If empty, all resource types are returned.",
|
||||
),
|
||||
resource_name: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by resource name (partial match supported). Useful for finding specific resources like 'prod-db' or 'test-bucket'.",
|
||||
),
|
||||
tag_key: str | None = Field(
|
||||
default=None,
|
||||
description="Filter resources by tag key (e.g., 'Environment', 'CostCenter', 'Owner').",
|
||||
),
|
||||
tag_value: str | None = Field(
|
||||
default=None,
|
||||
description="Filter resources by tag value (e.g., 'production', 'staging', 'development').",
|
||||
),
|
||||
date_from: str | None = Field(
|
||||
default=None,
|
||||
description="Start date for range query in ISO 8601 format (YYYY-MM-DD, e.g., '2025-01-15'). Full date required. IMPORTANT: Maximum date range is 2 days. If only date_from is provided, date_to is automatically set to 2 days later.",
|
||||
),
|
||||
date_to: str | None = Field(
|
||||
default=None,
|
||||
description="End date for range query in ISO 8601 format (YYYY-MM-DD, e.g., '2025-01-15'). Full date required. If only date_to is provided, date_from is automatically set to 2 days earlier.",
|
||||
),
|
||||
search: str | None = Field(
|
||||
default=None, description="Free-text search term across resource details"
|
||||
),
|
||||
page_size: int = Field(
|
||||
default=50, description="Number of results to return per page (max 1000)"
|
||||
),
|
||||
page_number: int = Field(
|
||||
default=1, description="Page number to retrieve (1-indexed)"
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""List and filter all resources scanned by Prowler.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT resource information. Use this for fast searching
|
||||
and filtering across many resources. For complete configuration details, metadata, and finding
|
||||
relationships, use prowler_app_get_resource on specific resources of interest.
|
||||
|
||||
This is the primary tool for browsing resources with rich filtering capabilities.
|
||||
Returns current state by default (latest scan per provider). Specify dates to query
|
||||
historical data (2-day maximum window).
|
||||
|
||||
Default behavior:
|
||||
- Returns latest resources from most recent scans (no date parameters needed)
|
||||
- Returns 50 results per page
|
||||
- Sorted by service, region, and name for logical grouping
|
||||
|
||||
Date filtering:
|
||||
- Without dates: queries resources from the most recent completed scan per provider (most efficient)
|
||||
- With dates: queries historical resource state (2-day maximum range between date_from and date_to)
|
||||
|
||||
Each resource includes:
|
||||
- Core identification: id (UUID for prowler_app_get_resource), uid, name
|
||||
- Location context: region, service, type
|
||||
- Security context: failed_findings_count (number of active security issues)
|
||||
- Tags: tags associated with the resource
|
||||
|
||||
Useful Workflow:
|
||||
1. Use this tool to search and filter resources by provider, region, service, tags, etc.
|
||||
2. Use prowler_app_get_resource with the resource 'id' to get complete configuration and metadata
|
||||
3. Use prowler_app_search_security_findings to find security issues for specific resources
|
||||
4. Use prowler_app_get_finding_details to get details about the security issues for specific resources
|
||||
"""
|
||||
# Validate page_size parameter
|
||||
self.api_client.validate_page_size(page_size)
|
||||
|
||||
# Determine endpoint based on date parameters
|
||||
date_range = self.api_client.normalize_date_range(
|
||||
date_from, date_to, max_days=2
|
||||
)
|
||||
|
||||
if date_range is None:
|
||||
# No dates provided - use latest resources endpoint
|
||||
endpoint = "/resources/latest"
|
||||
params = {}
|
||||
else:
|
||||
# Dates provided - use historical resources endpoint
|
||||
endpoint = "/resources"
|
||||
params = {
|
||||
"filter[updated_at__gte]": date_range[0],
|
||||
"filter[updated_at__lte]": date_range[1],
|
||||
}
|
||||
|
||||
# Build filter parameters
|
||||
if provider_type:
|
||||
params["filter[provider_type__in]"] = provider_type
|
||||
if provider_alias:
|
||||
params["filter[provider_alias__icontains]"] = provider_alias
|
||||
if provider_uid:
|
||||
params["filter[provider_uid__icontains]"] = provider_uid
|
||||
if region:
|
||||
params["filter[region__in]"] = region
|
||||
if service:
|
||||
params["filter[service__in]"] = service
|
||||
if resource_type:
|
||||
params["filter[type__in]"] = resource_type
|
||||
if resource_name:
|
||||
params["filter[name__icontains]"] = resource_name
|
||||
if tag_key:
|
||||
params["filter[tag_key]"] = tag_key
|
||||
if tag_value:
|
||||
params["filter[tag_value]"] = tag_value
|
||||
if search:
|
||||
params["filter[search]"] = search
|
||||
|
||||
# Pagination
|
||||
params["page[size]"] = page_size
|
||||
params["page[number]"] = page_number
|
||||
|
||||
# Return only LLM-relevant fields
|
||||
params["fields[resources]"] = (
|
||||
"uid,name,region,service,type,failed_findings_count,tags"
|
||||
)
|
||||
params["sort"] = "service,region,name"
|
||||
|
||||
# Convert lists to comma-separated strings
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
# Get API response and transform to simplified format
|
||||
api_response = await self.api_client.get(endpoint, params=clean_params)
|
||||
simplified_response = ResourcesListResponse.from_api_response(api_response)
|
||||
|
||||
return simplified_response.model_dump()
|
||||
|
||||
async def get_resource(
|
||||
self,
|
||||
resource_id: str = Field(
|
||||
description="Prowler's internal UUID (v4) for the resource to retrieve, generated when the resource was discovered in the system. Use `prowler_app_list_resources` tool to find the right ID"
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Retrieve comprehensive details about a specific resource by its ID.
|
||||
|
||||
IMPORTANT: This tool provides COMPLETE resource details with all available information.
|
||||
Use this after finding a specific resource via prowler_app_list_resources.
|
||||
|
||||
This tool provides ALL information that prowler_app_list_resources returns PLUS:
|
||||
|
||||
1. Configuration Details:
|
||||
- metadata: Provider-specific configuration (tags, policies, encryption settings, network rules)
|
||||
- partition: Provider-specific partition/region grouping (e.g., aws, aws-cn, aws-us-gov for AWS)
|
||||
|
||||
2. Temporal Tracking:
|
||||
- inserted_at: When Prowler first discovered this resource
|
||||
- updated_at: When resource configuration last changed
|
||||
|
||||
3. Security Relationships:
|
||||
- finding_ids: Prowler's internal UUIDs (v4) of all security findings associated with this resource
|
||||
- Use prowler_app_get_finding_details on these IDs to get remediation guidance
|
||||
|
||||
Useful Workflow:
|
||||
1. Use prowler_app_list_resources to browse and filter across many resources
|
||||
2. Use this tool to drill down into specific resources of interest
|
||||
3. Use prowler_app_get_finding_details to get details about the security issues for specific resources
|
||||
"""
|
||||
params = {}
|
||||
|
||||
# Get API response and transform to detailed format
|
||||
api_response = await self.api_client.get(
|
||||
f"/resources/{resource_id}", params=params
|
||||
)
|
||||
detailed_resource = DetailedResource.from_api_response(
|
||||
api_response.get("data", {})
|
||||
)
|
||||
|
||||
return detailed_resource.model_dump()
|
||||
|
||||
async def get_resources_overview(
|
||||
self,
|
||||
provider_type: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by provider type. Multiple values allowed. If empty, all providers are returned. For valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
|
||||
),
|
||||
provider_alias: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by specific provider alias/name (partial match supported).",
|
||||
),
|
||||
provider_uid: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by provider's native ID (e.g., AWS account ID, Azure subscription ID).",
|
||||
),
|
||||
date_from: str | None = Field(
|
||||
default=None,
|
||||
description="Start date for range query in ISO 8601 format (YYYY-MM-DD). Maximum 2-day range.",
|
||||
),
|
||||
date_to: str | None = Field(
|
||||
default=None,
|
||||
description="End date for range query in ISO 8601 format (YYYY-MM-DD).",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a markdown overview of your resources with statistics and insights.
|
||||
|
||||
IMPORTANT: This tool provides HIGH-LEVEL STATISTICS without returning individual resources.
|
||||
Use this when you need a summary view before drilling into details.
|
||||
|
||||
The report includes:
|
||||
- Total number of resources
|
||||
- Available services across your providers
|
||||
- Regions where resources are deployed
|
||||
- Resource types present in your providers
|
||||
|
||||
Output format: Markdown-formatted report ready to present to users or include in documentation.
|
||||
|
||||
Use cases:
|
||||
- Understanding infrastructure footprint
|
||||
- Identifying resource concentration (which regions, services)
|
||||
- Multi-provider deployment auditing
|
||||
- Resource inventory reporting
|
||||
- Tags planning (by provider, service, region)
|
||||
"""
|
||||
# Determine endpoint based on date parameters
|
||||
date_range = self.api_client.normalize_date_range(
|
||||
date_from, date_to, max_days=2
|
||||
)
|
||||
|
||||
if date_range is None:
|
||||
# No dates provided - use latest metadata endpoint
|
||||
metadata_endpoint = "/resources/metadata/latest"
|
||||
list_endpoint = "/resources/latest"
|
||||
params = {}
|
||||
else:
|
||||
# Dates provided - use historical endpoints
|
||||
metadata_endpoint = "/resources/metadata"
|
||||
list_endpoint = "/resources"
|
||||
params = {
|
||||
"filter[updated_at__gte]": date_range[0],
|
||||
"filter[updated_at__lte]": date_range[1],
|
||||
}
|
||||
|
||||
# Build common filter parameters
|
||||
if provider_type:
|
||||
params["filter[provider_type__in]"] = provider_type
|
||||
if provider_alias:
|
||||
params["filter[provider_alias__icontains]"] = provider_alias
|
||||
if provider_uid:
|
||||
params["filter[provider_uid__icontains]"] = provider_uid
|
||||
|
||||
# Convert lists to comma-separated strings
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
# Get metadata (services, regions, types)
|
||||
metadata_params = clean_params.copy()
|
||||
metadata_params["fields[resources-metadata]"] = "services,regions,types"
|
||||
metadata_response = await self.api_client.get(
|
||||
metadata_endpoint, params=metadata_params
|
||||
)
|
||||
metadata = ResourcesMetadataResponse.from_api_response(metadata_response)
|
||||
|
||||
# Get total count (using page_size=1 for efficiency)
|
||||
count_params = clean_params.copy()
|
||||
count_params["page[size]"] = 1
|
||||
count_params["page[number]"] = 1
|
||||
count_response = await self.api_client.get(list_endpoint, params=count_params)
|
||||
total_resources = (
|
||||
count_response.get("meta", {}).get("pagination", {}).get("count", 0)
|
||||
)
|
||||
|
||||
# Build markdown report
|
||||
report_lines = ["# Cloud Resources Overview", ""]
|
||||
|
||||
# Total resources
|
||||
report_lines.append(f"**Total Resources**: {total_resources:,} resources")
|
||||
report_lines.append("")
|
||||
|
||||
# Services
|
||||
if metadata.services:
|
||||
report_lines.append("## Services")
|
||||
report_lines.append(f"**{len(metadata.services)}** unique services found")
|
||||
report_lines.append("")
|
||||
for i, service in enumerate(metadata.services, 1):
|
||||
report_lines.append(f"{i}. {service}")
|
||||
report_lines.append("")
|
||||
|
||||
# Regions
|
||||
if metadata.regions:
|
||||
report_lines.append("## Regions")
|
||||
report_lines.append(f"**{len(metadata.regions)}** unique regions found")
|
||||
report_lines.append("")
|
||||
for i, region in enumerate(metadata.regions, 1):
|
||||
report_lines.append(f"{i}. {region}")
|
||||
report_lines.append("")
|
||||
|
||||
# Resource types
|
||||
if metadata.types:
|
||||
report_lines.append("## Resource Types")
|
||||
report_lines.append(
|
||||
f"**{len(metadata.types)}** unique resource types found"
|
||||
)
|
||||
report_lines.append("")
|
||||
for i, rtype in enumerate(metadata.types, 1):
|
||||
report_lines.append(f"{i}. {rtype}")
|
||||
report_lines.append("")
|
||||
|
||||
report = "\n".join(report_lines)
|
||||
return {"report": report}
|
||||
@@ -1,327 +0,0 @@
|
||||
"""Security Scans tools for Prowler App MCP Server.
|
||||
|
||||
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,
|
||||
ScansListResponse,
|
||||
ScheduleCreationResult,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
|
||||
|
||||
class ScansTools(BaseTool):
|
||||
"""Tools for security scan operations.
|
||||
|
||||
Provides tools for:
|
||||
- prowler_app_list_scans: Search and filter scans with rich filtering capabilities
|
||||
- prowler_app_get_scan: Get comprehensive details about a specific scan
|
||||
- prowler_app_trigger_scan: Trigger manual security scans for providers
|
||||
- prowler_app_schedule_daily_scan: Schedule automated daily scans for continuous monitoring
|
||||
- prowler_app_update_scan: Update scan names for better organization
|
||||
"""
|
||||
|
||||
async def list_scans(
|
||||
self,
|
||||
provider_id: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by Prowler's internal UUID(s) (v4) for specific provider(s), generated when the provider was registered. Use `prowler_app_search_providers` tool to find provider IDs",
|
||||
),
|
||||
provider_type: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by cloud provider type. For all valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
|
||||
),
|
||||
provider_alias: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by provider alias/friendly name. Partial match supported (case-insensitive)",
|
||||
),
|
||||
state: list[
|
||||
Literal[
|
||||
"available",
|
||||
"scheduled",
|
||||
"executing",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
]
|
||||
] = Field(
|
||||
default=[],
|
||||
description="Filter by scan execution state.",
|
||||
),
|
||||
trigger: Literal["manual", "scheduled"] | None = Field(
|
||||
default=None,
|
||||
description="Filter by how the scan was initiated. Options: 'manual' (user-initiated via prowler_app_trigger_scan), 'scheduled' (automated via prowler_app_schedule_daily_scan)",
|
||||
),
|
||||
name: str | None = Field(
|
||||
default=None,
|
||||
description="Filter by scan name. Partial match supported (case-insensitive)",
|
||||
),
|
||||
page_size: int = Field(
|
||||
default=50,
|
||||
description="Number of results to return per page",
|
||||
),
|
||||
page_number: int = Field(
|
||||
default=1,
|
||||
description="Page number to retrieve (1-indexed)",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""List and filter security scans across all providers with rich filtering capabilities.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT scan information. Use this for fast searching and filtering
|
||||
across many scans. For complete scan details including progress, duration, and resource counts,
|
||||
use prowler_app_get_scan on specific scans of interest.
|
||||
|
||||
Default behavior:
|
||||
- Returns all scans
|
||||
- Returns 50 scans per page
|
||||
- Includes all scan states (available, scheduled, executing, completed, failed, cancelled)
|
||||
|
||||
Each scan includes:
|
||||
- Core identification: id (UUID for prowler_app_get_scan), name
|
||||
- Execution context: state, trigger (manual/scheduled)
|
||||
- Temporal data: started_at, completed_at
|
||||
- Provider relationship: provider_id
|
||||
|
||||
Workflow:
|
||||
1. Use this tool to search and filter scans by provider, state, or date range
|
||||
2. Use prowler_app_get_scan with the scan 'id' to get progress, duration, and resource counts
|
||||
3. Use prowler_app_search_security_findings filtered by scan dates to analyze scan results
|
||||
"""
|
||||
# Validate pagination
|
||||
self.api_client.validate_page_size(page_size)
|
||||
|
||||
# Build query parameters
|
||||
params: dict[str, Any] = {
|
||||
"page[size]": page_size,
|
||||
"page[number]": page_number,
|
||||
}
|
||||
|
||||
# Apply provider filters
|
||||
if provider_id:
|
||||
params["filter[provider__in]"] = provider_id
|
||||
if provider_type:
|
||||
params["filter[provider_type__in]"] = provider_type
|
||||
if provider_alias:
|
||||
params["filter[provider_alias__icontains]"] = provider_alias
|
||||
|
||||
# Apply scan filters
|
||||
if state:
|
||||
params["filter[state__in]"] = state
|
||||
if trigger:
|
||||
params["filter[trigger]"] = trigger
|
||||
if name:
|
||||
params["filter[name__icontains]"] = name
|
||||
|
||||
clean_params = self.api_client.build_filter_params(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()
|
||||
|
||||
async def get_scan(
|
||||
self,
|
||||
scan_id: str = Field(
|
||||
description="Prowler's internal UUID (v4) for the scan to retrieve, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_app_list_scans` tool to find scan IDs"
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Retrieve comprehensive details about a specific scan by its ID.
|
||||
|
||||
IMPORTANT: This tool returns COMPLETE scan details.
|
||||
Use this after finding a specific scan via prowler_app_list_scans.
|
||||
|
||||
This tool provides ALL information that prowler_app_list_scans returns PLUS:
|
||||
|
||||
1. Execution Details:
|
||||
- progress: Scan completion progress as percentage (0-100%)
|
||||
- duration: Total scan duration in seconds from start to completion
|
||||
- unique_resource_count: Number of unique cloud resources discovered during the scan
|
||||
|
||||
2. Temporal Metadata:
|
||||
- inserted_at: When the scan was created in the database
|
||||
- scheduled_at: When the scan was scheduled to run (for scheduled scans)
|
||||
- next_scan_at: When the next scan will run (for recurring daily scans)
|
||||
|
||||
Useful for:
|
||||
- Monitoring scan progress during execution (via progress field)
|
||||
- Viewing scan results and metrics after completion
|
||||
- Debugging failed scans with detailed state information
|
||||
- Understanding scan scheduling patterns
|
||||
|
||||
Workflow:
|
||||
1. Use prowler_app_list_scans to browse and filter scans
|
||||
2. Use this tool with the scan 'id' to monitor progress or view detailed results
|
||||
3. For completed scans, use prowler_app_search_security_findings filtered by date to analyze findings
|
||||
"""
|
||||
# Fetch scan with all fields
|
||||
params = {
|
||||
"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"/scans/{scan_id}", params=params)
|
||||
detailed_scan = DetailedScan.from_api_response(api_response["data"])
|
||||
|
||||
return detailed_scan.model_dump()
|
||||
|
||||
async def trigger_scan(
|
||||
self,
|
||||
provider_id: str = Field(
|
||||
description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_app_search_providers` tool to find the provider ID"
|
||||
),
|
||||
name: str | None = Field(
|
||||
default=None,
|
||||
description="Optional human-friendly name for the scan. Use descriptive names to identify scan purpose or context, e.g., 'Weekly Production Security Audit', 'Pre-Deployment Validation', 'Compliance Check Q4 2025'",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Trigger a manual security scan for a provider.
|
||||
|
||||
IMPORTANT: This tool returns immediately once the scan is created.
|
||||
The scan will continue running in the background. Use `prowler_app_get_scan`
|
||||
with the returned scan ID to monitor progress and check when it completes.
|
||||
|
||||
Example Useful Workflow:
|
||||
1. Use `prowler_app_search_providers` to find the provider_id you want to scan
|
||||
2. Use this tool to trigger the scan
|
||||
3. Use `prowler_app_get_scan` with the returned scan 'id' to monitor progress
|
||||
4. Once completed, use `prowler_app_search_security_findings` to analyze results
|
||||
"""
|
||||
try:
|
||||
# Build request data
|
||||
request_data: dict[str, Any] = {
|
||||
"data": {
|
||||
"type": "scans",
|
||||
"attributes": {},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"id": provider_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if name:
|
||||
request_data["data"]["attributes"]["name"] = name
|
||||
|
||||
# Create scan (returns Task)
|
||||
self.logger.info(f"Creating scan for provider {provider_id}")
|
||||
task_response = await self.api_client.post("/scans", json_data=request_data)
|
||||
|
||||
scan_id = (
|
||||
task_response.get("data", {})
|
||||
.get("attributes", {})
|
||||
.get("task_args", {})
|
||||
.get("scan_id", None)
|
||||
)
|
||||
|
||||
if not scan_id:
|
||||
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"/scans/{scan_id}")
|
||||
scan_info = DetailedScan.from_api_response(scan_response["data"])
|
||||
|
||||
return ScanCreationResult(
|
||||
scan=scan_info,
|
||||
status="success",
|
||||
message=f"Scan {scan_id} created successfully. The scan may take some time to complete. Use prowler_app_get_scan tool with this ID to monitor progress.",
|
||||
).model_dump()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Scan creation failed: {e}")
|
||||
return ScanCreationResult(
|
||||
scan=None,
|
||||
status="failed",
|
||||
message=f"Scan creation failed: {str(e)}",
|
||||
).model_dump()
|
||||
|
||||
async def schedule_daily_scan(
|
||||
self,
|
||||
provider_id: str = Field(
|
||||
description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_app_search_providers` tool to find the provider ID"
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Schedule automated daily scans for a provider for continuous security monitoring.
|
||||
|
||||
Creates a recurring daily scan schedule that will automatically trigger
|
||||
scans every 24 hours (starting from the moment the schedule is created).
|
||||
The schedule persists until manually removed and will execute even when
|
||||
you're not actively using the system.
|
||||
|
||||
IMPORTANT: This tool returns immediately once the daily schedule is created.
|
||||
The schedule will be set up in the background. Use `prowler_app_list_scans`
|
||||
filtered by provider_id and trigger='scheduled' to view scheduled scans.
|
||||
|
||||
IMPORTANT: This creates a PERSISTENT schedule. The provider will be scanned
|
||||
automatically every 24 hours until the provider is deleted.
|
||||
|
||||
Example Useful Workflow:
|
||||
1. Use `prowler_app_search_providers` to find the provider_id you want to monitor
|
||||
2. Use this tool to create the daily schedule
|
||||
3. Use `prowler_app_list_scans` filtered by provider_id to view scheduled and completed scans
|
||||
4. Monitor findings over time with `prowler_app_search_security_findings`
|
||||
"""
|
||||
self.logger.info(f"Creating daily schedule for provider {provider_id}")
|
||||
task_response = await self.api_client.post(
|
||||
"/schedules/daily",
|
||||
json_data={
|
||||
"data": {
|
||||
"type": "daily-schedules",
|
||||
"attributes": {
|
||||
"provider_id": provider_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
task_state = (
|
||||
task_response.get("data", {}).get("attributes", {}).get("state", None)
|
||||
)
|
||||
|
||||
if task_state == "available":
|
||||
return_message = "Daily schedule created successfully. The schedule is being set up in the background. Use prowler_app_list_scans with provider_id filter to view scheduled scans."
|
||||
else:
|
||||
return_message = "Daily schedule creation failed. Please try again later."
|
||||
|
||||
return ScheduleCreationResult(
|
||||
scheduled=(task_state == "available"),
|
||||
message=return_message,
|
||||
).model_dump()
|
||||
|
||||
async def update_scan(
|
||||
self,
|
||||
scan_id: str = Field(
|
||||
description="Prowler's internal UUID (v4) for the scan to update, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_app_list_scans` tool to find the scan ID if you only know the provider or scan name. Returns an error if the scan ID is invalid or not found."
|
||||
),
|
||||
name: str = Field(
|
||||
description="New human-friendly name for the scan (3-100 characters). Use descriptive names to improve organization and tracking, e.g., 'Production Security Audit - Q4 2025', 'Post-Deployment Compliance Check'. IMPORTANT: Only the scan name can be updated - other attributes (state, progress, duration) are read-only and managed by the system."
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Update a scan's name for better organization and tracking.
|
||||
|
||||
IMPORTANT: Only the scan name can be updated. Other scan attributes
|
||||
(state, progress, duration, etc.) are read-only and managed by the system.
|
||||
|
||||
Example Useful Workflow:
|
||||
1. Use `prowler_app_list_scans` to find the scan you want to rename
|
||||
2. Use this tool with the scan 'id' and new name
|
||||
"""
|
||||
api_response = await self.api_client.patch(
|
||||
f"/scans/{scan_id}",
|
||||
json_data={
|
||||
"data": {
|
||||
"type": "scans",
|
||||
"id": scan_id,
|
||||
"attributes": {"name": name},
|
||||
},
|
||||
},
|
||||
)
|
||||
detailed_scan = DetailedScan.from_api_response(api_response["data"])
|
||||
|
||||
return detailed_scan.model_dump()
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Shared API client utilities for Prowler App tools."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
@@ -84,13 +83,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
if not response.content:
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": response.status_code,
|
||||
}
|
||||
else:
|
||||
return response.json()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"HTTP error during {method.value} {path}: {e}")
|
||||
error_detail: str = ""
|
||||
@@ -187,68 +180,6 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
"""
|
||||
return await self._make_request(HTTPMethod.DELETE, path, params=params)
|
||||
|
||||
async def poll_task_until_complete(
|
||||
self,
|
||||
task_id: str,
|
||||
timeout: int = 60,
|
||||
poll_interval: float = 1.0,
|
||||
) -> dict[str, any]:
|
||||
"""Poll a task until it reaches a terminal state.
|
||||
|
||||
This method polls the task endpoint at regular intervals until the task
|
||||
completes, fails, or times out. It's designed for async operations like
|
||||
provider connection tests and deletions that return task IDs.
|
||||
|
||||
Args:
|
||||
task_id: The UUID of the task to poll (UUID object or string)
|
||||
timeout: Maximum time to wait in seconds (default: 60)
|
||||
poll_interval: Time between polls in seconds (default: 1.0)
|
||||
|
||||
Returns:
|
||||
The complete task response when terminal state is reached
|
||||
|
||||
Raises:
|
||||
Exception: If task fails, is cancelled, or timeout is exceeded
|
||||
"""
|
||||
terminal_states = {"completed", "failed", "cancelled"}
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
max_time = start_time + timeout
|
||||
|
||||
logger.info(
|
||||
f"Polling task {task_id} (timeout: {timeout}s, interval: {poll_interval}s)"
|
||||
)
|
||||
|
||||
while True:
|
||||
# Check if we've exceeded the timeout
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
if current_time >= max_time:
|
||||
raise Exception(
|
||||
f"Task {task_id} polling timed out after {timeout} seconds. "
|
||||
f"The task may still be running. Try increasing the timeout or check task status manually."
|
||||
)
|
||||
|
||||
# Fetch current task state
|
||||
response = await self.get(f"/tasks/{task_id}")
|
||||
task_data = response.get("data", {})
|
||||
task_attrs = task_data.get("attributes", {})
|
||||
state = task_attrs.get("state")
|
||||
|
||||
logger.debug(f"Task {task_id} state: {state}")
|
||||
|
||||
# Check if we've reached a terminal state
|
||||
if state in terminal_states:
|
||||
if state == "completed":
|
||||
logger.info(f"Task {task_id} completed successfully")
|
||||
return response
|
||||
elif state == "failed":
|
||||
error_msg = task_attrs.get("error", "Unknown error")
|
||||
raise Exception(f"Task {task_id} failed: {error_msg}")
|
||||
elif state == "cancelled":
|
||||
raise Exception(f"Task {task_id} was cancelled")
|
||||
|
||||
# Wait before next poll
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
def _validate_date_format(self, date_str: str, param_name: str) -> datetime:
|
||||
"""Validate date string format.
|
||||
|
||||
@@ -322,14 +253,6 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
elif to_date and not from_date:
|
||||
from_date = to_date - timedelta(days=max_days - 1)
|
||||
|
||||
# Validate that date_from is before or equal to date_to
|
||||
if from_date > to_date:
|
||||
raise ValueError(
|
||||
f"Invalid date range: date_from must be before or equal to date_to. "
|
||||
f"Got date_from='{from_date.date()}' and date_to='{to_date.date()}'. "
|
||||
f"Please swap the dates or use the correct order."
|
||||
)
|
||||
|
||||
# Validate range doesn't exceed max_days
|
||||
delta: int = (to_date - from_date).days + 1
|
||||
if delta > max_days:
|
||||
|
||||
@@ -15,7 +15,7 @@ class ProwlerAppAuth:
|
||||
def __init__(
|
||||
self,
|
||||
mode: str = os.getenv("PROWLER_MCP_TRANSPORT_MODE", "stdio"),
|
||||
base_url: str = os.getenv("API_BASE_URL", "https://api.prowler.com/api/v1"),
|
||||
base_url: str = os.getenv("PROWLER_API_BASE_URL", "https://api.prowler.com"),
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
logger.info(f"Using Prowler App API base URL: {self.base_url}")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
from prowler_mcp_server import __version__
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -9,7 +11,7 @@ class SearchResult(BaseModel):
|
||||
path: str = Field(description="Document path")
|
||||
title: str = Field(description="Document title")
|
||||
url: str = Field(description="Documentation URL")
|
||||
highlights: list[str] = Field(
|
||||
highlights: List[str] = Field(
|
||||
description="Highlighted content snippets showing query matches with <mark><b> tags",
|
||||
default_factory=list,
|
||||
)
|
||||
@@ -52,7 +54,7 @@ class ProwlerDocsSearchEngine:
|
||||
},
|
||||
)
|
||||
|
||||
def search(self, query: str, page_size: int = 5) -> list[SearchResult]:
|
||||
def search(self, query: str, page_size: int = 5) -> List[SearchResult]:
|
||||
"""
|
||||
Search documentation using Mintlify API.
|
||||
|
||||
@@ -61,7 +63,7 @@ class ProwlerDocsSearchEngine:
|
||||
page_size: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
list of search results
|
||||
List of search results
|
||||
"""
|
||||
try:
|
||||
# Construct request body
|
||||
@@ -137,7 +139,7 @@ class ProwlerDocsSearchEngine:
|
||||
print(f"Search error: {e}")
|
||||
return []
|
||||
|
||||
def get_document(self, doc_path: str) -> str | None:
|
||||
def get_document(self, doc_path: str) -> Optional[str]:
|
||||
"""
|
||||
Get full document content from Mintlify documentation.
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_documentation.search_engine import (
|
||||
ProwlerDocsSearchEngine,
|
||||
)
|
||||
@@ -14,44 +12,46 @@ prowler_docs_search_engine = ProwlerDocsSearchEngine()
|
||||
|
||||
@docs_mcp_server.tool()
|
||||
def search(
|
||||
term: str = Field(description="The term to search for in the documentation"),
|
||||
page_size: int = Field(
|
||||
5,
|
||||
description="Number of top results to return to return. It must be between 1 and 20.",
|
||||
gt=1,
|
||||
lt=20,
|
||||
),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Search in Prowler documentation.
|
||||
query: str,
|
||||
page_size: int = 5,
|
||||
) -> List[dict[str, Any]]:
|
||||
"""
|
||||
Search in Prowler documentation.
|
||||
|
||||
This tool searches through the official Prowler documentation
|
||||
to find relevant information about everything related to Prowler.
|
||||
to find relevant information about security checks, cloud providers,
|
||||
compliance frameworks, and usage instructions.
|
||||
|
||||
Uses fulltext search to find the most relevant documentation pages
|
||||
based on your query.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
page_size: Number of top results to return (default: 5)
|
||||
|
||||
Returns:
|
||||
List of search results with highlights showing matched terms (in <mark><b> tags)
|
||||
"""
|
||||
return prowler_docs_search_engine.search(term, page_size) # type: ignore In the hint we cannot put SearchResult type because JSON API MCP Generator cannot handle Pydantic models yet
|
||||
return prowler_docs_search_engine.search(query, page_size)
|
||||
|
||||
|
||||
@docs_mcp_server.tool()
|
||||
def get_document(
|
||||
doc_path: str = Field(
|
||||
description="Path to the documentation file to retrieve. It is the same as the 'path' field of the search results. Use `prowler_docs_search` to find the path first."
|
||||
),
|
||||
) -> dict[str, str]:
|
||||
"""Retrieve the full content of a Prowler documentation file.
|
||||
doc_path: str,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieve the full content of a Prowler documentation file.
|
||||
|
||||
Use this after searching to get the complete content of a specific
|
||||
documentation file.
|
||||
|
||||
Args:
|
||||
doc_path: Path to the documentation file. It is the same as the "path" field of the search results.
|
||||
|
||||
Returns:
|
||||
Full content of the documentation file in markdown format.
|
||||
Full content of the documentation file
|
||||
"""
|
||||
content: str | None = prowler_docs_search_engine.get_document(doc_path)
|
||||
content = prowler_docs_search_engine.get_document(doc_path)
|
||||
if content is None:
|
||||
return {"error": f"Document '{doc_path}' not found."}
|
||||
else:
|
||||
return {"content": content}
|
||||
raise ValueError(f"Document not found: {doc_path}")
|
||||
return content
|
||||
|
||||
@@ -4,10 +4,10 @@ Prowler Hub MCP module
|
||||
Provides access to Prowler Hub API for security checks and compliance frameworks.
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server import __version__
|
||||
|
||||
# Initialize FastMCP for Prowler Hub
|
||||
@@ -55,90 +55,109 @@ def github_check_path(provider_id: str, check_id: str, suffix: str) -> str:
|
||||
return f"{GITHUB_RAW_BASE}/{provider_id}/services/{service_id}/{check_id}/{check_id}{suffix}"
|
||||
|
||||
|
||||
# Security Check Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def list_checks(
|
||||
providers: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by Prowler provider IDs. Example: ['aws', 'azure']. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
),
|
||||
services: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by provider services. Example: ['s3', 'ec2', 'keyvault']. Use `prowler_hub_get_provider_services` to get available services for a provider.",
|
||||
),
|
||||
severities: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by severity levels. Example: ['high', 'critical']. Available: 'low', 'medium', 'high', 'critical'.",
|
||||
),
|
||||
categories: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by security categories. Example: ['encryption', 'internet-exposed'].",
|
||||
),
|
||||
compliances: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by compliance framework IDs. Example: ['cis_4.0_aws', 'ens_rd2022_azure']. Use `prowler_hub_list_compliances` to get available compliance IDs.",
|
||||
),
|
||||
) -> dict:
|
||||
"""List security Prowler Checks with filtering capabilities.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT check data. Use this for fast browsing and filtering.
|
||||
For complete details including risk, remediation guidance, and categories use `prowler_hub_get_check_details`.
|
||||
|
||||
IMPORTANT: An unfiltered request returns 1000+ checks. Use filters to narrow results.
|
||||
async def get_check_filters() -> dict[str, Any]:
|
||||
"""
|
||||
Get available values for filtering for tool `get_checks`. Recommended to use before calling `get_checks` to get the available values for the filters.
|
||||
|
||||
Returns:
|
||||
Available filter options including providers, types, services, severities,
|
||||
categories, and compliance frameworks with their respective counts
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get("/check/filters")
|
||||
response.raise_for_status()
|
||||
filters = response.json()
|
||||
|
||||
return {"filters": filters}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Security Check Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def get_checks(
|
||||
providers: Optional[str] = None,
|
||||
types: Optional[str] = None,
|
||||
services: Optional[str] = None,
|
||||
severities: Optional[str] = None,
|
||||
categories: Optional[str] = None,
|
||||
compliances: Optional[str] = None,
|
||||
ids: Optional[str] = None,
|
||||
fields: Optional[str] = "id,service,severity,title,description,risk",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
List security Prowler Checks. The list can be filtered by the parameters defined for the tool.
|
||||
It is recommended to use the tool `get_check_filters` to get the available values for the filters.
|
||||
A not filtered request will return more than 1000 checks, so it is recommended to use the filters.
|
||||
|
||||
Args:
|
||||
providers: Filter by Prowler provider IDs. Example: "aws,azure". Use the tool `list_providers` to get the available providers IDs.
|
||||
types: Filter by check types.
|
||||
services: Filter by provider services IDs. Example: "s3,keyvault". Use the tool `list_providers` to get the available services IDs in a provider.
|
||||
severities: Filter by severity levels. Example: "medium,high". Available values are "low", "medium", "high", "critical".
|
||||
categories: Filter by categories. Example: "cluster-security,encryption".
|
||||
compliances: Filter by compliance framework IDs. Example: "cis_4.0_aws,ens_rd2022_azure".
|
||||
ids: Filter by specific check IDs. Example: "s3_bucket_level_public_access_block".
|
||||
fields: Specify which fields from checks metadata to return (id is always included). Example: "id,title,description,risk".
|
||||
Available values are "id", "title", "description", "provider", "type", "service", "subservice", "severity", "risk", "reference", "remediation", "services_required", "aws_arn_template", "notes", "categories", "default_value", "resource_type", "related_url", "depends_on", "related_to", "fixer".
|
||||
The default parameters are "id,title,description".
|
||||
If null, all fields will be returned.
|
||||
|
||||
Returns:
|
||||
List of security checks matching the filters. The structure is as follows:
|
||||
{
|
||||
"count": N,
|
||||
"checks": [
|
||||
{
|
||||
"id": "check_id",
|
||||
"provider": "provider_id",
|
||||
"title": "Human-readable check title",
|
||||
"severity": "critical|high|medium|low",
|
||||
},
|
||||
{"id": "check_id_1", "title": "check_title_1", "description": "check_description_1", ...},
|
||||
{"id": "check_id_2", "title": "check_title_2", "description": "check_description_2", ...},
|
||||
{"id": "check_id_3", "title": "check_title_3", "description": "check_description_3", ...},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Useful Example Workflow:
|
||||
1. Use `prowler_hub_list_providers` to see available Prowler providers
|
||||
2. Use `prowler_hub_get_provider_services` to see services for a provider
|
||||
3. Use this tool with filters to find relevant checks
|
||||
4. Use `prowler_hub_get_check_details` to get complete information for a specific check
|
||||
"""
|
||||
# Lightweight fields for listing
|
||||
lightweight_fields = "id,title,severity,provider"
|
||||
|
||||
params: dict[str, str] = {"fields": lightweight_fields}
|
||||
params: dict[str, str] = {}
|
||||
|
||||
if providers:
|
||||
params["providers"] = ",".join(providers)
|
||||
params["providers"] = providers
|
||||
if types:
|
||||
params["types"] = types
|
||||
if services:
|
||||
params["services"] = ",".join(services)
|
||||
params["services"] = services
|
||||
if severities:
|
||||
params["severities"] = ",".join(severities)
|
||||
params["severities"] = severities
|
||||
if categories:
|
||||
params["categories"] = ",".join(categories)
|
||||
params["categories"] = categories
|
||||
if compliances:
|
||||
params["compliances"] = ",".join(compliances)
|
||||
params["compliances"] = compliances
|
||||
if ids:
|
||||
params["ids"] = ids
|
||||
if fields:
|
||||
params["fields"] = fields
|
||||
|
||||
try:
|
||||
response = prowler_hub_client.get("/check", params=params)
|
||||
response.raise_for_status()
|
||||
checks = response.json()
|
||||
|
||||
# Return checks as a lightweight list
|
||||
checks_list = []
|
||||
checks_dict = {}
|
||||
for check in checks:
|
||||
check_data = {
|
||||
"id": check["id"],
|
||||
"provider": check["provider"],
|
||||
"title": check["title"],
|
||||
"severity": check["severity"],
|
||||
}
|
||||
checks_list.append(check_data)
|
||||
check_data = {}
|
||||
# Always include the id field as it's mandatory for the response structure
|
||||
if "id" in check:
|
||||
check_data["id"] = check["id"]
|
||||
|
||||
return {"count": len(checks), "checks": checks_list}
|
||||
# Include other requested fields
|
||||
for field in fields.split(","):
|
||||
if field != "id" and field in check: # Skip id since it's already added
|
||||
check_data[field] = check[field]
|
||||
checks_dict[check["id"]] = check_data
|
||||
|
||||
return {"count": len(checks), "checks": checks_dict}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
@@ -148,220 +167,60 @@ async def list_checks(
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def semantic_search_checks(
|
||||
term: str = Field(
|
||||
description="Search term. Examples: 'public access', 'encryption', 'MFA', 'logging'.",
|
||||
),
|
||||
) -> dict:
|
||||
"""Search for security checks using free-text search across all metadata.
|
||||
async def get_check_raw_metadata(
|
||||
provider_id: str,
|
||||
check_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch the raw check metadata JSON, this is a low level version of the tool `get_checks`.
|
||||
It is recommended to use the tool `get_checks` filtering about the `ids` parameter instead of using this tool.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT check data. Use this for discovering checks by topic.
|
||||
For complete details including risk, remediation guidance, and categories use `prowler_hub_get_check_details`.
|
||||
|
||||
Searches across check titles, descriptions, risk statements, remediation guidance,
|
||||
and other text fields. Use this when you don't know the exact check ID or want to
|
||||
explore checks related to a topic.
|
||||
Args:
|
||||
provider_id: Prowler provider ID (e.g., "aws", "azure").
|
||||
check_id: Prowler check ID (folder and base filename).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"count": N,
|
||||
"checks": [
|
||||
{
|
||||
"id": "check_id",
|
||||
"provider": "provider_id",
|
||||
"title": "Human-readable check title",
|
||||
"severity": "critical|high|medium|low",
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Useful Example Workflow:
|
||||
1. Use this tool to search for checks by keyword or topic
|
||||
2. Use `prowler_hub_list_checks` with filters for more targeted browsing
|
||||
3. Use `prowler_hub_get_check_details` to get complete information for a specific check
|
||||
Raw metadata JSON as stored in Prowler.
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get("/check/search", params={"term": term})
|
||||
response.raise_for_status()
|
||||
checks = response.json()
|
||||
|
||||
# Return checks as a lightweight list
|
||||
checks_list = []
|
||||
for check in checks:
|
||||
check_data = {
|
||||
"id": check["id"],
|
||||
"provider": check["provider"],
|
||||
"title": check["title"],
|
||||
"severity": check["severity"],
|
||||
if provider_id and check_id:
|
||||
url = github_check_path(provider_id, check_id, ".metadata.json")
|
||||
try:
|
||||
resp = github_raw_client.get(url)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
return {
|
||||
"error": f"Check {check_id} not found in Prowler",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": f"Error fetching check {check_id} from Prowler: {str(e)}",
|
||||
}
|
||||
checks_list.append(check_data)
|
||||
|
||||
return {"count": len(checks), "checks": checks_list}
|
||||
except httpx.HTTPStatusError as e:
|
||||
else:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
"error": "Provider ID and check ID are required",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_details(
|
||||
check_id: str = Field(
|
||||
description="The check ID to retrieve details for. Example: 's3_bucket_level_public_access_block'"
|
||||
),
|
||||
) -> dict:
|
||||
"""Retrieve comprehensive details about a specific security check by its ID.
|
||||
|
||||
IMPORTANT: This tool returns COMPLETE check details.
|
||||
Use this after finding a specific check ID, you can get it via `prowler_hub_list_checks` or `prowler_hub_semantic_search_checks`.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"id": "string",
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"provider": "string",
|
||||
"service": "string",
|
||||
"severity": "low",
|
||||
"risk": "string",
|
||||
"reference": [
|
||||
"string"
|
||||
],
|
||||
"additional_urls": [
|
||||
"string"
|
||||
],
|
||||
"remediation": {
|
||||
"cli": {
|
||||
"description": "string"
|
||||
},
|
||||
"terraform": {
|
||||
"description": "string"
|
||||
},
|
||||
"nativeiac": {
|
||||
"description": "string"
|
||||
},
|
||||
"other": {
|
||||
"description": "string"
|
||||
},
|
||||
"wui": {
|
||||
"description": "string",
|
||||
"reference": "string"
|
||||
}
|
||||
},
|
||||
"services_required": [
|
||||
"string"
|
||||
],
|
||||
"notes": "string",
|
||||
"compliances": [
|
||||
{
|
||||
"name": "string",
|
||||
"id": "string"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
"string"
|
||||
],
|
||||
"resource_type": "string",
|
||||
"related_url": "string",
|
||||
"fixer": bool
|
||||
}
|
||||
|
||||
Useful Example Workflow:
|
||||
1. Use `prowler_hub_list_checks` or `prowler_hub_search_checks` to find check IDs
|
||||
2. Use this tool with the check 'id' to get complete information including remediation guidance
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get(f"/check/{check_id}")
|
||||
response.raise_for_status()
|
||||
check = response.json()
|
||||
|
||||
if not check:
|
||||
return {"error": f"Check '{check_id}' not found"}
|
||||
|
||||
# Build response with only non-empty fields to save tokens
|
||||
result = {}
|
||||
|
||||
# Core fields
|
||||
result["id"] = check["id"]
|
||||
if check.get("title"):
|
||||
result["title"] = check["title"]
|
||||
if check.get("description"):
|
||||
result["description"] = check["description"]
|
||||
if check.get("provider"):
|
||||
result["provider"] = check["provider"]
|
||||
if check.get("service"):
|
||||
result["service"] = check["service"]
|
||||
if check.get("severity"):
|
||||
result["severity"] = check["severity"]
|
||||
if check.get("risk"):
|
||||
result["risk"] = check["risk"]
|
||||
if check.get("resource_type"):
|
||||
result["resource_type"] = check["resource_type"]
|
||||
|
||||
# List fields
|
||||
if check.get("reference"):
|
||||
result["reference"] = check["reference"]
|
||||
if check.get("additional_urls"):
|
||||
result["additional_urls"] = check["additional_urls"]
|
||||
if check.get("services_required"):
|
||||
result["services_required"] = check["services_required"]
|
||||
if check.get("categories"):
|
||||
result["categories"] = check["categories"]
|
||||
if check.get("compliances"):
|
||||
result["compliances"] = check["compliances"]
|
||||
|
||||
# Other fields
|
||||
if check.get("notes"):
|
||||
result["notes"] = check["notes"]
|
||||
if check.get("related_url"):
|
||||
result["related_url"] = check["related_url"]
|
||||
if check.get("fixer") is not None:
|
||||
result["fixer"] = check["fixer"]
|
||||
|
||||
# Remediation - filter out empty nested values
|
||||
remediation = check.get("remediation", {})
|
||||
if remediation:
|
||||
filtered_remediation = {}
|
||||
for key, value in remediation.items():
|
||||
if value and isinstance(value, dict):
|
||||
# Filter out empty values within nested dict
|
||||
filtered_value = {k: v for k, v in value.items() if v}
|
||||
if filtered_value:
|
||||
filtered_remediation[key] = filtered_value
|
||||
elif value:
|
||||
filtered_remediation[key] = value
|
||||
if filtered_remediation:
|
||||
result["remediation"] = filtered_remediation
|
||||
|
||||
return result
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_code(
|
||||
provider_id: str = Field(
|
||||
description="Prowler Provider ID. Example: 'aws', 'azure', 'gcp', 'kubernetes'. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
),
|
||||
check_id: str = Field(
|
||||
description="The check ID. Example: 's3_bucket_public_access'. Get IDs from `prowler_hub_list_checks` or `prowler_hub_search_checks`.",
|
||||
),
|
||||
) -> dict:
|
||||
"""Fetch the Python implementation code of a Prowler security check.
|
||||
provider_id: str,
|
||||
check_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch the check implementation Python code from Prowler.
|
||||
|
||||
The check code shows exactly how Prowler evaluates resources for security issues.
|
||||
Use this to understand check logic, customize checks, or create new ones.
|
||||
Args:
|
||||
provider_id: Prowler provider ID (e.g., "aws", "azure").
|
||||
check_id: Prowler check ID (e.g., "opensearch_service_domains_not_publicly_accessible").
|
||||
|
||||
Returns:
|
||||
{
|
||||
"content": "Python source code of the check implementation"
|
||||
}
|
||||
Dict with the code content as text.
|
||||
"""
|
||||
if provider_id and check_id:
|
||||
url = github_check_path(provider_id, check_id, ".py")
|
||||
@@ -392,29 +251,18 @@ async def get_check_code(
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_fixer(
|
||||
provider_id: str = Field(
|
||||
description="Prowler Provider ID. Example: 'aws', 'azure', 'gcp', 'kubernetes'. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
),
|
||||
check_id: str = Field(
|
||||
description="The check ID. Example: 's3_bucket_public_access'. Get IDs from `prowler_hub_list_checks` or `prowler_hub_search_checks`.",
|
||||
),
|
||||
) -> dict:
|
||||
"""Fetch the auto-remediation (fixer) code for a Prowler security check.
|
||||
provider_id: str,
|
||||
check_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch the check fixer Python code from Prowler, if it exists.
|
||||
|
||||
IMPORTANT: Not all checks have fixers. A "fixer not found" response means the check
|
||||
doesn't have auto-remediation code - this is normal for many checks.
|
||||
|
||||
Fixer code provides automated remediation that can fix security issues detected by checks.
|
||||
Use this to understand how to programmatically remediate findings.
|
||||
Args:
|
||||
provider_id: Prowler provider ID (e.g., "aws", "azure").
|
||||
check_id: Prowler check ID (e.g., "opensearch_service_domains_not_publicly_accessible").
|
||||
|
||||
Returns:
|
||||
{
|
||||
"content": "Python source code of the auto-remediation implementation"
|
||||
}
|
||||
Or if no fixer exists:
|
||||
{
|
||||
"error": "Fixer not found for check {check_id}"
|
||||
}
|
||||
Dict with fixer content as text if present, existence flag.
|
||||
"""
|
||||
if provider_id and check_id:
|
||||
url = github_check_path(provider_id, check_id, "_fixer.py")
|
||||
@@ -447,66 +295,95 @@ async def get_check_fixer(
|
||||
}
|
||||
|
||||
|
||||
# Compliance Framework Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def list_compliances(
|
||||
provider: list[str] = Field(
|
||||
default=[],
|
||||
description="Filter by cloud provider. Example: ['aws']. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
),
|
||||
) -> dict:
|
||||
"""List compliance frameworks supported by Prowler.
|
||||
async def search_checks(term: str) -> dict[str, Any]:
|
||||
"""
|
||||
Search the term across all text properties of check metadata.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT compliance data. Use this for fast browsing and filtering.
|
||||
For complete details including requirements use `prowler_hub_get_compliance_details`.
|
||||
|
||||
Compliance frameworks define sets of security requirements that checks map to.
|
||||
Use this to discover available frameworks for compliance reporting.
|
||||
|
||||
WARNING: An unfiltered request may return a large number of frameworks. Use the provider with not more than 3 different providers to make easier the response handling.
|
||||
Args:
|
||||
term: Search term to find in check titles, descriptions, and other text fields
|
||||
|
||||
Returns:
|
||||
List of checks matching the search term
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get("/check/search", params={"term": term})
|
||||
response.raise_for_status()
|
||||
checks = response.json()
|
||||
|
||||
return {
|
||||
"count": len(checks),
|
||||
"checks": checks,
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Compliance Framework Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def get_compliance_frameworks(
|
||||
provider: Optional[str] = None,
|
||||
fields: Optional[
|
||||
str
|
||||
] = "id,framework,provider,description,total_checks,total_requirements",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
List and filter compliance frameworks. The list can be filtered by the parameters defined for the tool.
|
||||
|
||||
Args:
|
||||
provider: Filter by one Prowler provider ID. Example: "aws". Use the tool `list_providers` to get the available providers IDs.
|
||||
fields: Specify which fields to return (id is always included). Example: "id,provider,description,version".
|
||||
It is recommended to run with the default parameters because the full response is too large.
|
||||
Available values are "id", "framework", "provider", "description", "total_checks", "total_requirements", "created_at", "updated_at".
|
||||
The default parameters are "id,framework,provider,description,total_checks,total_requirements".
|
||||
If null, all fields will be returned.
|
||||
|
||||
Returns:
|
||||
List of compliance frameworks. The structure is as follows:
|
||||
{
|
||||
"count": N,
|
||||
"compliances": [
|
||||
{
|
||||
"id": "cis_4.0_aws",
|
||||
"name": "CIS Amazon Web Services Foundations Benchmark v4.0",
|
||||
"provider": "aws",
|
||||
},
|
||||
...
|
||||
]
|
||||
"frameworks": {
|
||||
"framework_id": {
|
||||
"id": "framework_id",
|
||||
"provider": "provider_id",
|
||||
"description": "framework_description",
|
||||
"version": "framework_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Useful Example Workflow:
|
||||
1. Use `prowler_hub_list_providers` to see available cloud providers
|
||||
2. Use this tool to browse compliance frameworks
|
||||
3. Use `prowler_hub_get_compliance_details` with the compliance 'id' to get complete information
|
||||
"""
|
||||
# Lightweight fields for listing
|
||||
lightweight_fields = "id,name,provider"
|
||||
|
||||
params: dict[str, str] = {"fields": lightweight_fields}
|
||||
params = {}
|
||||
|
||||
if provider:
|
||||
params["provider"] = ",".join(provider)
|
||||
params["provider"] = provider
|
||||
if fields:
|
||||
params["fields"] = fields
|
||||
|
||||
try:
|
||||
response = prowler_hub_client.get("/compliance", params=params)
|
||||
response.raise_for_status()
|
||||
compliances = response.json()
|
||||
frameworks = response.json()
|
||||
|
||||
# Return compliances as a lightweight list
|
||||
compliances_list = []
|
||||
for compliance in compliances:
|
||||
compliance_data = {
|
||||
"id": compliance["id"],
|
||||
"name": compliance["name"],
|
||||
"provider": compliance["provider"],
|
||||
}
|
||||
compliances_list.append(compliance_data)
|
||||
frameworks_dict = {}
|
||||
for framework in frameworks:
|
||||
framework_data = {}
|
||||
# Always include the id field as it's mandatory for the response structure
|
||||
if "id" in framework:
|
||||
framework_data["id"] = framework["id"]
|
||||
|
||||
return {"count": len(compliances), "compliances": compliances_list}
|
||||
# Include other requested fields
|
||||
for field in fields.split(","):
|
||||
if (
|
||||
field != "id" and field in framework
|
||||
): # Skip id since it's already added
|
||||
framework_data[field] = framework[field]
|
||||
frameworks_dict[framework["id"]] = framework_data
|
||||
|
||||
return {"count": len(frameworks), "frameworks": frameworks_dict}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
@@ -516,140 +393,27 @@ async def list_compliances(
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def semantic_search_compliances(
|
||||
term: str = Field(
|
||||
description="Search term. Examples: 'CIS', 'HIPAA', 'PCI', 'GDPR', 'SOC2', 'NIST'.",
|
||||
),
|
||||
) -> dict:
|
||||
"""Search for compliance frameworks using free-text search.
|
||||
async def search_compliance_frameworks(term: str) -> dict[str, Any]:
|
||||
"""
|
||||
Search compliance frameworks by term.
|
||||
|
||||
IMPORTANT: This tool returns LIGHTWEIGHT compliance data. Use this for discovering frameworks by topic.
|
||||
For complete details including requirements use `prowler_hub_get_compliance_details`.
|
||||
|
||||
Searches across framework names, descriptions, and metadata. Use this when you
|
||||
want to find frameworks related to a specific regulation, standard, or topic.
|
||||
Args:
|
||||
term: Search term to find in framework names and descriptions
|
||||
|
||||
Returns:
|
||||
{
|
||||
"count": N,
|
||||
"compliances": [
|
||||
{
|
||||
"id": "cis_4.0_aws",
|
||||
"name": "CIS Amazon Web Services Foundations Benchmark v4.0",
|
||||
"provider": "aws",
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
List of compliance frameworks matching the search term
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get("/compliance/search", params={"term": term})
|
||||
response.raise_for_status()
|
||||
compliances = response.json()
|
||||
frameworks = response.json()
|
||||
|
||||
# Return compliances as a lightweight list
|
||||
compliances_list = []
|
||||
for compliance in compliances:
|
||||
compliance_data = {
|
||||
"id": compliance["id"],
|
||||
"name": compliance["name"],
|
||||
"provider": compliance["provider"],
|
||||
}
|
||||
compliances_list.append(compliance_data)
|
||||
|
||||
return {"count": len(compliances), "compliances": compliances_list}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
"count": len(frameworks),
|
||||
"search_term": term,
|
||||
"frameworks": frameworks,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_compliance_details(
|
||||
compliance_id: str = Field(
|
||||
description="The compliance framework ID to retrieve details for. Example: 'cis_4.0_aws'. Use `prowler_hub_list_compliances` or `prowler_hub_semantic_search_compliances` to find available compliance IDs.",
|
||||
),
|
||||
) -> dict:
|
||||
"""Retrieve comprehensive details about a specific compliance framework by its ID.
|
||||
|
||||
IMPORTANT: This tool returns COMPLETE compliance details.
|
||||
Use this after finding a specific compliance via `prowler_hub_list_compliances` or `prowler_hub_semantic_search_compliances`.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"framework": "string",
|
||||
"provider": "string",
|
||||
"version": "string",
|
||||
"description": "string",
|
||||
"total_checks": int,
|
||||
"total_requirements": int,
|
||||
"requirements": [
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"checks": ["check_id_1", "check_id_2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get(f"/compliance/{compliance_id}")
|
||||
response.raise_for_status()
|
||||
compliance = response.json()
|
||||
|
||||
if not compliance:
|
||||
return {"error": f"Compliance '{compliance_id}' not found"}
|
||||
|
||||
# Build response with only non-empty fields to save tokens
|
||||
result = {}
|
||||
|
||||
# Core fields
|
||||
result["id"] = compliance["id"]
|
||||
if compliance.get("name"):
|
||||
result["name"] = compliance["name"]
|
||||
if compliance.get("framework"):
|
||||
result["framework"] = compliance["framework"]
|
||||
if compliance.get("provider"):
|
||||
result["provider"] = compliance["provider"]
|
||||
if compliance.get("version"):
|
||||
result["version"] = compliance["version"]
|
||||
if compliance.get("description"):
|
||||
result["description"] = compliance["description"]
|
||||
|
||||
# Numeric fields
|
||||
if compliance.get("total_checks"):
|
||||
result["total_checks"] = compliance["total_checks"]
|
||||
if compliance.get("total_requirements"):
|
||||
result["total_requirements"] = compliance["total_requirements"]
|
||||
|
||||
# Requirements - filter out empty nested values
|
||||
requirements = compliance.get("requirements", [])
|
||||
if requirements:
|
||||
filtered_requirements = []
|
||||
for req in requirements:
|
||||
filtered_req = {}
|
||||
if req.get("id"):
|
||||
filtered_req["id"] = req["id"]
|
||||
if req.get("name"):
|
||||
filtered_req["name"] = req["name"]
|
||||
if req.get("description"):
|
||||
filtered_req["description"] = req["description"]
|
||||
if req.get("checks"):
|
||||
filtered_req["checks"] = req["checks"]
|
||||
if filtered_req:
|
||||
filtered_requirements.append(filtered_req)
|
||||
if filtered_requirements:
|
||||
result["requirements"] = filtered_requirements
|
||||
|
||||
return result
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
return {"error": f"Compliance '{compliance_id}' not found"}
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
@@ -659,28 +423,20 @@ async def get_compliance_details(
|
||||
|
||||
# Provider Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def list_providers() -> dict:
|
||||
"""List all providers supported by Prowler.
|
||||
|
||||
This is a reference tool that shows available providers (aws, azure, gcp, kubernetes, etc.)
|
||||
that can be scanned for finding security issues.
|
||||
|
||||
Use the provider IDs from this tool as filter values in other tools.
|
||||
async def list_providers() -> dict[str, Any]:
|
||||
"""
|
||||
Get all available Prowler providers and their associated services.
|
||||
|
||||
Returns:
|
||||
List of Prowler providers with their associated services. The structure is as follows:
|
||||
{
|
||||
"count": N,
|
||||
"providers": [
|
||||
{
|
||||
"id": "aws",
|
||||
"name": "Amazon Web Services"
|
||||
},
|
||||
{
|
||||
"id": "azure",
|
||||
"name": "Microsoft Azure"
|
||||
},
|
||||
...
|
||||
]
|
||||
"providers": {
|
||||
"provider_id": {
|
||||
"name": "provider_name",
|
||||
"services": ["service_id_1", "service_id_2", "service_id_3", ...]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
@@ -688,16 +444,14 @@ async def list_providers() -> dict:
|
||||
response.raise_for_status()
|
||||
providers = response.json()
|
||||
|
||||
providers_list = []
|
||||
providers_dict = {}
|
||||
for provider in providers:
|
||||
providers_list.append(
|
||||
{
|
||||
"id": provider["id"],
|
||||
"name": provider.get("name", ""),
|
||||
}
|
||||
)
|
||||
providers_dict[provider["id"]] = {
|
||||
"name": provider.get("name", ""),
|
||||
"services": provider.get("services", []),
|
||||
}
|
||||
|
||||
return {"count": len(providers), "providers": providers_list}
|
||||
return {"count": len(providers), "providers": providers_dict}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
@@ -706,42 +460,24 @@ async def list_providers() -> dict:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Analytics Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def get_provider_services(
|
||||
provider_id: str = Field(
|
||||
description="The provider ID to get services for. Example: 'aws', 'azure', 'gcp', 'kubernetes'. Use `prowler_hub_list_providers` to get available provider IDs.",
|
||||
),
|
||||
) -> dict:
|
||||
"""Get the list of services IDs available for a specific cloud provider.
|
||||
|
||||
Services represent the different resources and capabilities that Prowler can scan
|
||||
within a provider (e.g., s3, ec2, iam for AWS or keyvault, storage for Azure).
|
||||
|
||||
Use service IDs from this tool as filter values in other tools.
|
||||
async def get_artifacts_count() -> dict[str, Any]:
|
||||
"""
|
||||
Get total count of security artifacts (checks + compliance frameworks).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"provider_id": "aws",
|
||||
"provider_name": "Amazon Web Services",
|
||||
"count": N,
|
||||
"services": ["s3", "ec2", "iam", "rds", "lambda", ...]
|
||||
}
|
||||
Total number of artifacts in the Prowler Hub.
|
||||
"""
|
||||
try:
|
||||
response = prowler_hub_client.get("/providers")
|
||||
response = prowler_hub_client.get("/n_artifacts")
|
||||
response.raise_for_status()
|
||||
providers = response.json()
|
||||
data = response.json()
|
||||
|
||||
for provider in providers:
|
||||
if provider["id"] == provider_id:
|
||||
return {
|
||||
"provider_id": provider["id"],
|
||||
"provider_name": provider.get("name", ""),
|
||||
"count": len(provider.get("services", [])),
|
||||
"services": provider.get("services", []),
|
||||
}
|
||||
|
||||
return {"error": f"Provider '{provider_id}' not found"}
|
||||
return {
|
||||
"total_artifacts": data.get("n", 0),
|
||||
"details": "Total count includes both security checks and compliance frameworks",
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
|
||||
@@ -11,7 +11,7 @@ description = "MCP server for Prowler ecosystem"
|
||||
name = "prowler-mcp"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
version = "0.3.0"
|
||||
version = "0.1.0"
|
||||
|
||||
[project.scripts]
|
||||
generate-prowler-app-mcp-server = "prowler_mcp_server.prowler_app.utils.server_generator:generate_server_file"
|
||||
|
||||
@@ -603,7 +603,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-mcp"
|
||||
version = "0.3.0"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
|
||||
@@ -2923,8 +2923,6 @@ python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
|
||||
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
|
||||
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -5515,7 +5513,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
|
||||
@@ -5524,7 +5521,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
|
||||
@@ -5533,7 +5529,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
|
||||
@@ -5542,7 +5537,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
|
||||
@@ -5551,7 +5545,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
|
||||
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
|
||||
@@ -6460,4 +6453,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">3.9.1,<3.13"
|
||||
content-hash = "1559a8799915bf0372eef07396e1dc40802911ef07ae92997cd260d9fe596ba3"
|
||||
content-hash = "433468987cb3c4499d094d90e9f8cc9062a25ce115fde991a4e1b39edbfb7815"
|
||||
|
||||
@@ -2,54 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.17.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Add Prowler ThreatScore for the Alibaba Cloud provider [(#9511)](https://github.com/prowler-cloud/prowler/pull/9511)
|
||||
|
||||
### Changed
|
||||
- Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432)
|
||||
- Update AWS Route 53 service metadata to new format [(#9406)](https://github.com/prowler-cloud/prowler/pull/9406)
|
||||
- Update AWS SQS service metadata to new format [(#9429)](https://github.com/prowler-cloud/prowler/pull/9429)
|
||||
|
||||
---
|
||||
|
||||
## [5.16.0] (Prowler v5.16.0)
|
||||
|
||||
### Added
|
||||
|
||||
- `privilege-escalation` and `ec2-imdsv1` categories for AWS checks [(#9537)](https://github.com/prowler-cloud/prowler/pull/9537)
|
||||
- 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)
|
||||
- Update AWS WAF service metadata to new format [(#9480)](https://github.com/prowler-cloud/prowler/pull/9480)
|
||||
- Update AWS WAF v2 service metadata to new format [(#9481)](https://github.com/prowler-cloud/prowler/pull/9481)
|
||||
|
||||
### Fixed
|
||||
- Fix typo `trustboundaries` category to `trust-boundaries` [(#9536)](https://github.com/prowler-cloud/prowler/pull/9536)
|
||||
- Fix incorrect `bedrock-agent` regional availability, now using official AWS docs instead of copying from `bedrock`
|
||||
- Store MongoDB Atlas provider regions as lowercase [(#9554)](https://github.com/prowler-cloud/prowler/pull/9554)
|
||||
- Store GCP Cloud Storage bucket regions as lowercase [(#9567)](https://github.com/prowler-cloud/prowler/pull/9567)
|
||||
|
||||
---
|
||||
|
||||
## [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)
|
||||
## [v5.15.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- `cloudstorage_uses_vpc_service_controls` check for GCP provider [(#9256)](https://github.com/prowler-cloud/prowler/pull/9256)
|
||||
@@ -58,10 +11,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `compute_instance_preemptible_vm_disabled` check for GCP provider [(#9342)](https://github.com/prowler-cloud/prowler/pull/9342)
|
||||
- `compute_instance_automatic_restart_enabled` check for GCP provider [(#9271)](https://github.com/prowler-cloud/prowler/pull/9271)
|
||||
- `compute_instance_deletion_protection_enabled` check for GCP provider [(#9358)](https://github.com/prowler-cloud/prowler/pull/9358)
|
||||
- Update SOC2 - Azure with Processing Integrity requirements [(#9463)](https://github.com/prowler-cloud/prowler/pull/9463)
|
||||
- Update SOC2 - GCP with Processing Integrity requirements [(#9464)](https://github.com/prowler-cloud/prowler/pull/9464)
|
||||
- Update SOC2 - AWS with Processing Integrity requirements [(#9462)](https://github.com/prowler-cloud/prowler/pull/9462)
|
||||
- RBI Cyber Security Framework compliance for Azure provider [(#8822)](https://github.com/prowler-cloud/prowler/pull/8822)
|
||||
|
||||
### Changed
|
||||
- Update AWS Macie service metadata to new format [(#9265)](https://github.com/prowler-cloud/prowler/pull/9265)
|
||||
@@ -72,22 +21,16 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Update AWS Macie service metadata to new format [(#9265)](https://github.com/prowler-cloud/prowler/pull/9265)
|
||||
- Update AWS Lightsail service metadata to new format [(#9264)](https://github.com/prowler-cloud/prowler/pull/9264)
|
||||
|
||||
### Fixed
|
||||
- Fix duplicate requirement IDs in ISO 27001:2013 AWS compliance framework by adding unique letter suffixes
|
||||
- Removed incorrect threat-detection category from checks metadata [(#9489)](https://github.com/prowler-cloud/prowler/pull/9489)
|
||||
- GCP `cloudstorage_uses_vpc_service_controls` check to handle VPC Service Controls blocked API access [(#9478)](https://github.com/prowler-cloud/prowler/pull/9478)
|
||||
|
||||
---
|
||||
|
||||
## [5.14.2] (Prowler v5.14.2)
|
||||
## [v5.14.2] (Prowler UNRELEASED)
|
||||
|
||||
### Fixed
|
||||
- Custom check folder metadata validation [(#9335)](https://github.com/prowler-cloud/prowler/pull/9335)
|
||||
- Pin `alibabacloud-gateway-oss-util` to version 0.0.3 to address missing dependency [(#9487)](https://github.com/prowler-cloud/prowler/pull/9487)
|
||||
|
||||
---
|
||||
|
||||
## [5.14.1] (Prowler v5.14.1)
|
||||
## [v5.14.1] (Prowler v5.14.1)
|
||||
|
||||
### Fixed
|
||||
- `sharepoint_external_sharing_managed` check to handle external sharing disabled at organization level [(#9298)](https://github.com/prowler-cloud/prowler/pull/9298)
|
||||
@@ -95,7 +38,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.14.0] (Prowler v5.14.0)
|
||||
## [v5.14.0] (Prowler v5.14.0)
|
||||
|
||||
### Added
|
||||
- GitHub provider check `organization_default_repository_permission_strict` [(#8785)](https://github.com/prowler-cloud/prowler/pull/8785)
|
||||
@@ -173,7 +116,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.13.1] (Prowler v5.13.1)
|
||||
## [v5.13.1] (Prowler v5.13.1)
|
||||
|
||||
### Fixed
|
||||
- Add `resource_name` for checks under `logging` for the GCP provider [(#9023)](https://github.com/prowler-cloud/prowler/pull/9023)
|
||||
@@ -189,7 +132,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.13.0] (Prowler v5.13.0)
|
||||
## [v5.13.0] (Prowler v5.13.0)
|
||||
|
||||
### Added
|
||||
- Support for AdditionalURLs in outputs [(#8651)](https://github.com/prowler-cloud/prowler/pull/8651)
|
||||
@@ -247,7 +190,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.12.1] (Prowler v5.12.1)
|
||||
## [v5.12.1] (Prowler v5.12.1)
|
||||
|
||||
### Fixed
|
||||
- Replaced old check id with new ones for compliance files [(#8682)](https://github.com/prowler-cloud/prowler/pull/8682)
|
||||
@@ -256,7 +199,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.12.0] (Prowler v5.12.0)
|
||||
## [v5.12.0] (Prowler v5.12.0)
|
||||
|
||||
### Added
|
||||
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
|
||||
@@ -292,7 +235,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.11.0] (Prowler v5.11.0)
|
||||
## [v5.11.0] (Prowler v5.11.0)
|
||||
|
||||
### Added
|
||||
- Certificate authentication for M365 provider [(#8404)](https://github.com/prowler-cloud/prowler/pull/8404)
|
||||
@@ -323,7 +266,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.10.2] (Prowler v5.10.2)
|
||||
## [v5.10.2] (Prowler v5.10.2)
|
||||
|
||||
### Fixed
|
||||
- Order requirements by ID in Prowler ThreatScore AWS compliance framework [(#8495)](https://github.com/prowler-cloud/prowler/pull/8495)
|
||||
@@ -337,14 +280,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.10.1] (Prowler v5.10.1)
|
||||
## [v5.10.1] (Prowler v5.10.1)
|
||||
|
||||
### Fixed
|
||||
- Remove invalid requirements from CIS 1.0 for GitHub provider [(#8472)](https://github.com/prowler-cloud/prowler/pull/8472)
|
||||
|
||||
---
|
||||
|
||||
## [5.10.0] (Prowler v5.10.0)
|
||||
## [v5.10.0] (Prowler v5.10.0)
|
||||
|
||||
### Added
|
||||
- `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321)
|
||||
@@ -384,14 +327,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.9.2] (Prowler v5.9.2)
|
||||
## [v5.9.2] (Prowler v5.9.2)
|
||||
|
||||
### Fixed
|
||||
- Use the correct resource name in `defender_domain_dkim_enabled` check [(#8334)](https://github.com/prowler-cloud/prowler/pull/8334)
|
||||
|
||||
---
|
||||
|
||||
## [5.9.0] (Prowler v5.9.0)
|
||||
## [v5.9.0] (Prowler v5.9.0)
|
||||
|
||||
### Added
|
||||
- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123)
|
||||
@@ -425,7 +368,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.8.1] (Prowler v5.8.1)
|
||||
## [v5.8.1] (Prowler 5.8.1)
|
||||
|
||||
### Fixed
|
||||
- Detect wildcarded ARNs in sts:AssumeRole policy resources [(#8164)](https://github.com/prowler-cloud/prowler/pull/8164)
|
||||
@@ -435,7 +378,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.8.0] (Prowler v5.8.0)
|
||||
## [v5.8.0] (Prowler v5.8.0)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -497,7 +440,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.7.5] (Prowler v5.7.5)
|
||||
## [v5.7.5] (Prowler v5.7.5)
|
||||
|
||||
### Fixed
|
||||
- Use unified timestamp for all requirements [(#8059)](https://github.com/prowler-cloud/prowler/pull/8059)
|
||||
@@ -515,7 +458,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.7.3] (Prowler v5.7.3)
|
||||
## [v5.7.3] (Prowler v5.7.3)
|
||||
|
||||
### Fixed
|
||||
- Automatically encrypt password in Microsoft365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
@@ -523,7 +466,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.7.2] (Prowler v5.7.2)
|
||||
## [v5.7.2] (Prowler v5.7.2)
|
||||
|
||||
### Fixed
|
||||
- `m365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
@@ -535,7 +478,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.7.0] (Prowler v5.7.0)
|
||||
## [v5.7.0] (Prowler v5.7.0)
|
||||
|
||||
### Added
|
||||
- Update the compliance list supported for each provider from docs [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
|
||||
@@ -563,7 +506,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.6.0] (Prowler v5.6.0)
|
||||
## [v5.6.0] (Prowler v5.6.0)
|
||||
|
||||
### Added
|
||||
- SOC2 compliance framework to Azure [(#7489)](https://github.com/prowler-cloud/prowler/pull/7489)
|
||||
@@ -632,7 +575,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.5.1] (Prowler v5.5.1)
|
||||
## [v5.5.1] (Prowler v5.5.1)
|
||||
|
||||
### Fixed
|
||||
- Default name to contacts in Azure Defender [(#7483)](https://github.com/prowler-cloud/prowler/pull/7483)
|
||||
|
||||
@@ -83,9 +83,6 @@ from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import (
|
||||
AzureMitreAttack,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_gcp import GCPMitreAttack
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_alibaba import (
|
||||
ProwlerThreatScoreAlibaba,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_aws import (
|
||||
ProwlerThreatScoreAWS,
|
||||
)
|
||||
@@ -1042,18 +1039,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
cis.batch_write_data_to_file()
|
||||
elif compliance_name == "prowler_threatscore_alibabacloud":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
prowler_threatscore = ProwlerThreatScoreAlibaba(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(prowler_threatscore)
|
||||
prowler_threatscore.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
|
||||
@@ -547,106 +547,6 @@
|
||||
"cloudwatch_log_group_retention_policy_specific_days_enabled",
|
||||
"kinesis_stream_data_retention_period"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_2",
|
||||
"Name": "PI1.2 System inputs are measured and recorded completely, accurately, and timely to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entity's objectives. This includes defining accuracy targets, monitoring input quality, and creating detailed records of each input event.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_2",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"apigateway_restapi_logging_enabled",
|
||||
"apigatewayv2_api_access_logging_enabled",
|
||||
"elbv2_logging_enabled",
|
||||
"elb_logging_enabled",
|
||||
"wafv2_webacl_logging_enabled",
|
||||
"waf_global_webacl_logging_enabled",
|
||||
"cloudtrail_s3_dataevents_write_enabled",
|
||||
"cloudfront_distributions_logging_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_3",
|
||||
"Name": "PI1.3 Data is processed completely, accurately, and timely as authorized to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to ensure data is processed completely, accurately, and timely. This includes defining processing specifications, identifying processing activities, detecting and correcting errors throughout processing, recording processing activities with accurate logs, and ensuring completeness and timeliness of processing.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_3",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudtrail_log_file_validation_enabled",
|
||||
"cloudtrail_cloudwatch_logging_enabled",
|
||||
"cloudwatch_log_metric_filter_unauthorized_api_calls",
|
||||
"cloudwatch_log_metric_filter_authentication_failures",
|
||||
"cloudwatch_log_metric_filter_policy_changes",
|
||||
"cloudwatch_log_metric_filter_root_usage",
|
||||
"config_recorder_all_regions_enabled",
|
||||
"rds_instance_integration_cloudwatch_logs",
|
||||
"rds_cluster_integration_cloudwatch_logs",
|
||||
"glue_etl_jobs_logging_enabled",
|
||||
"stepfunctions_statemachine_logging_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_4",
|
||||
"Name": "PI1.4 System outputs are complete, accurate, distributed only to intended parties, and retained to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to ensure system outputs are delivered to authorized recipients in the correct format and protected against unauthorized access, modification, theft, destruction, or corruption. This includes output encryption, access controls, and audit trails for output delivery.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_4",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"s3_bucket_default_encryption",
|
||||
"s3_bucket_kms_encryption",
|
||||
"cloudwatch_log_group_kms_encryption_enabled",
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"kinesis_stream_encrypted_at_rest",
|
||||
"cloudfront_distributions_field_level_encryption_enabled",
|
||||
"cloudwatch_log_group_not_publicly_accessible",
|
||||
"cloudwatch_cross_account_sharing_disabled",
|
||||
"glue_etl_jobs_cloudwatch_logs_encryption_enabled",
|
||||
"glue_etl_jobs_amazon_s3_encryption_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_5",
|
||||
"Name": "PI1.5 Stored data is maintained complete, accurate, and protected from unauthorized modification to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to protect stored inputs, items in processing, and outputs from theft, destruction, corruption, or deterioration. This includes data encryption at rest, key management, backup and recovery procedures, access controls, and data integrity validation.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_5",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"s3_bucket_object_versioning",
|
||||
"s3_bucket_object_lock",
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_cluster_storage_encrypted",
|
||||
"dynamodb_tables_kms_cmk_encryption_enabled",
|
||||
"ec2_ebs_volume_encryption",
|
||||
"backup_plans_exist",
|
||||
"backup_recovery_point_encrypted",
|
||||
"backup_vaults_encrypted",
|
||||
"kms_cmk_rotation_enabled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
{
|
||||
"Framework": "RBI-Cyber-Security-Framework",
|
||||
"Name": "Reserve Bank of India (RBI) Cyber Security Framework",
|
||||
"Version": "",
|
||||
"Provider": "Azure",
|
||||
"Description": "The Reserve Bank had prescribed a set of baseline cyber security controls for primary (Urban) cooperative banks (UCBs) in October 2018. On further examination, it has been decided to prescribe a comprehensive cyber security framework for the UCBs, as a graded approach, based on their digital depth and interconnectedness with the payment systems landscape, digital products offered by them and assessment of cyber security risk. The framework would mandate implementation of progressively stronger security measures based on the nature, variety and scale of digital product offerings of banks.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "annex_i_1_1",
|
||||
"Name": "Annex I (1.1)",
|
||||
"Description": "UCBs should maintain an up-to-date business IT Asset Inventory Register containing the following fields, as a minimum: a) Details of the IT Asset (viz., hardware/software/network devices, key personnel, services, etc.), b. Details of systems where customer data are stored, c. Associated business applications, if any, d. Criticality of the IT asset (For example, High/Medium/Low).",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_1_1",
|
||||
"Service": "vm"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"vm_ensure_using_approved_images",
|
||||
"vm_ensure_using_managed_disks",
|
||||
"vm_trusted_launch_enabled",
|
||||
"aks_cluster_rbac_enabled",
|
||||
"aks_clusters_created_with_private_nodes",
|
||||
"appinsights_ensure_is_configured",
|
||||
"containerregistry_admin_user_disabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_1_3",
|
||||
"Name": "Annex I (1.3)",
|
||||
"Description": "Appropriately manage and provide protection within and outside UCB/network, keeping in mind how the data/information is stored, transmitted, processed, accessed and put to use within/outside the UCB's network, and level of risk they are exposed to depending on the sensitivity of the data/information.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_1_3",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"keyvault_key_rotation_enabled",
|
||||
"keyvault_access_only_through_private_endpoints",
|
||||
"keyvault_private_endpoints",
|
||||
"keyvault_rbac_enabled",
|
||||
"app_function_not_publicly_accessible",
|
||||
"app_ensure_http_is_redirected_to_https",
|
||||
"app_minimum_tls_version_12",
|
||||
"storage_blob_public_access_level_is_disabled",
|
||||
"storage_secure_transfer_required_is_enabled",
|
||||
"storage_ensure_encryption_with_customer_managed_keys",
|
||||
"storage_ensure_minimum_tls_version_12",
|
||||
"storage_default_network_access_rule_is_denied",
|
||||
"storage_ensure_private_endpoints_in_storage_accounts",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"sqlserver_unrestricted_inbound_access",
|
||||
"sqlserver_tde_encryption_enabled",
|
||||
"sqlserver_tde_encrypted_with_cmk",
|
||||
"cosmosdb_account_use_private_endpoints",
|
||||
"cosmosdb_account_firewall_use_selected_networks",
|
||||
"mysql_flexible_server_ssl_connection_enabled",
|
||||
"mysql_flexible_server_minimum_tls_version_12",
|
||||
"postgresql_flexible_server_enforce_ssl_enabled",
|
||||
"aks_clusters_public_access_disabled",
|
||||
"containerregistry_not_publicly_accessible",
|
||||
"containerregistry_uses_private_link",
|
||||
"aisearch_service_not_publicly_accessible"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_5_1",
|
||||
"Name": "Annex I (5.1)",
|
||||
"Description": "The firewall configurations should be set to the highest security level and evaluation of critical device (such as firewall, network switches, security devices, etc.) configurations should be done periodically.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_5_1",
|
||||
"Service": "network"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_http_internet_access_restricted",
|
||||
"network_udp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_flow_log_captured_sent",
|
||||
"network_flow_log_more_than_90_days",
|
||||
"network_watcher_enabled",
|
||||
"network_bastion_host_exists",
|
||||
"aks_network_policy_enabled",
|
||||
"storage_default_network_access_rule_is_denied"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_6",
|
||||
"Name": "Annex I (6)",
|
||||
"Description": "Put in place systems and processes to identify, track, manage and monitor the status of patches to servers, operating system and application software running at the systems used by the UCB officials (end-users). Implement and update antivirus protection for all servers and applicable end points preferably through a centralised system.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_6",
|
||||
"Service": "defender"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"defender_ensure_system_updates_are_applied",
|
||||
"defender_assessments_vm_endpoint_protection_installed",
|
||||
"defender_ensure_defender_for_server_is_on",
|
||||
"defender_ensure_defender_for_app_services_is_on",
|
||||
"defender_ensure_defender_for_sql_servers_is_on",
|
||||
"defender_ensure_defender_for_azure_sql_databases_is_on",
|
||||
"defender_ensure_defender_for_storage_is_on",
|
||||
"defender_ensure_defender_for_containers_is_on",
|
||||
"defender_ensure_defender_for_keyvault_is_on",
|
||||
"defender_ensure_defender_for_arm_is_on",
|
||||
"defender_ensure_defender_for_dns_is_on",
|
||||
"defender_ensure_defender_for_databases_is_on",
|
||||
"defender_ensure_defender_for_cosmosdb_is_on",
|
||||
"defender_container_images_scan_enabled",
|
||||
"defender_container_images_resolved_vulnerabilities",
|
||||
"defender_auto_provisioning_vulnerabilty_assessments_machines_on",
|
||||
"vm_backup_enabled",
|
||||
"app_ensure_java_version_is_latest",
|
||||
"app_ensure_php_version_is_latest",
|
||||
"app_ensure_python_version_is_latest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_7_1",
|
||||
"Name": "Annex I (7.1)",
|
||||
"Description": "Disallow administrative rights on end-user workstations/PCs/laptops and provide access rights on a 'need to know' and 'need to do' basis.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_7_1",
|
||||
"Service": "iam"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_role_user_access_admin_restricted",
|
||||
"iam_subscription_roles_owner_custom_not_created",
|
||||
"iam_custom_role_has_permissions_to_administer_resource_locks",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_policy_ensure_default_user_cannot_create_apps",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
"entra_policy_default_users_cannot_create_security_groups",
|
||||
"entra_policy_guest_invite_only_for_admin_roles",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"app_function_identity_without_admin_privileges"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_7_2",
|
||||
"Name": "Annex I (7.2)",
|
||||
"Description": "Passwords should be set as complex and lengthy and users should not use same passwords for all the applications/systems/devices.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_7_2",
|
||||
"Service": "entra"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_policy_user_consent_for_verified_apps",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
"entra_security_defaults_enabled",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_trusted_named_locations_exists",
|
||||
"sqlserver_azuread_administrator_enabled",
|
||||
"postgresql_flexible_server_entra_id_authentication_enabled",
|
||||
"cosmosdb_account_use_aad_and_rbac"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_7_3",
|
||||
"Name": "Annex I (7.3)",
|
||||
"Description": "Remote Desktop Protocol (RDP) which allows others to access the computer remotely over a network or over the internet should be always disabled and should be enabled only with the approval of the authorised officer of the UCB. Logs for such remote access shall be enabled and monitored for suspicious activities.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_7_3",
|
||||
"Service": "network"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"network_rdp_internet_access_restricted",
|
||||
"vm_jit_access_enabled",
|
||||
"network_bastion_host_exists",
|
||||
"vm_linux_enforce_ssh_authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_7_4",
|
||||
"Name": "Annex I (7.4)",
|
||||
"Description": "Implement appropriate (e.g. centralised) systems and controls to allow, manage, log and monitor privileged/super user/administrative access to critical systems (servers/databases, applications, network devices etc.)",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_7_4",
|
||||
"Service": "monitor"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"monitor_alert_create_update_nsg",
|
||||
"monitor_alert_delete_nsg",
|
||||
"monitor_diagnostic_setting_with_appropriate_categories",
|
||||
"monitor_diagnostic_settings_exists",
|
||||
"monitor_alert_create_policy_assignment",
|
||||
"monitor_alert_delete_policy_assignment",
|
||||
"monitor_alert_create_update_security_solution",
|
||||
"monitor_alert_delete_security_solution",
|
||||
"monitor_alert_create_update_sqlserver_fr",
|
||||
"monitor_alert_delete_sqlserver_fr",
|
||||
"monitor_alert_create_update_public_ip_address_rule",
|
||||
"monitor_alert_delete_public_ip_address_rule",
|
||||
"monitor_alert_service_health_exists",
|
||||
"monitor_storage_account_with_activity_logs_cmk_encrypted",
|
||||
"monitor_storage_account_with_activity_logs_is_private",
|
||||
"keyvault_logging_enabled",
|
||||
"sqlserver_auditing_enabled",
|
||||
"sqlserver_auditing_retention_90_days",
|
||||
"app_http_logs_enabled",
|
||||
"app_function_application_insights_enabled",
|
||||
"defender_additional_email_configured_with_a_security_contact",
|
||||
"defender_ensure_notify_alerts_severity_is_high",
|
||||
"defender_ensure_notify_emails_to_owners",
|
||||
"defender_ensure_mcas_is_enabled",
|
||||
"defender_ensure_wdatp_is_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "annex_i_12",
|
||||
"Name": "Annex I (12)",
|
||||
"Description": "Take periodic back up of the important data and store this data 'off line' (i.e., transferring important files to a storage device that can be detached from a computer/system after copying all the files).",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "annex_i_12",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"vm_backup_enabled",
|
||||
"vm_sufficient_daily_backup_retention_period",
|
||||
"storage_ensure_file_shares_soft_delete_is_enabled",
|
||||
"storage_blob_versioning_is_enabled",
|
||||
"storage_ensure_soft_delete_is_enabled",
|
||||
"storage_geo_redundant_enabled",
|
||||
"keyvault_recoverable",
|
||||
"sqlserver_vulnerability_assessment_enabled",
|
||||
"sqlserver_va_periodic_recurring_scans_enabled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -619,92 +619,6 @@
|
||||
"sqlserver_auditing_retention_90_days",
|
||||
"storage_ensure_soft_delete_is_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_2",
|
||||
"Name": "PI1.2 System inputs are measured and recorded completely, accurately, and timely to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entity's objectives. This includes defining accuracy targets, monitoring input quality, and creating detailed records of each input event.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_2",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "azure",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"app_http_logs_enabled",
|
||||
"network_flow_log_captured_sent",
|
||||
"keyvault_logging_enabled",
|
||||
"monitor_diagnostic_settings_exists",
|
||||
"sqlserver_auditing_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_3",
|
||||
"Name": "PI1.3 Data is processed completely, accurately, and timely as authorized to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to ensure data is processed completely, accurately, and timely. This includes defining processing specifications, identifying processing activities, detecting and correcting errors throughout processing, recording processing activities with accurate logs, and ensuring completeness and timeliness of processing.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_3",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "azure",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"monitor_diagnostic_setting_with_appropriate_categories",
|
||||
"monitor_diagnostic_settings_exists",
|
||||
"defender_auto_provisioning_log_analytics_agent_vms_on",
|
||||
"mysql_flexible_server_audit_log_enabled",
|
||||
"postgresql_flexible_server_log_checkpoints_on",
|
||||
"postgresql_flexible_server_log_connections_on",
|
||||
"postgresql_flexible_server_log_disconnections_on",
|
||||
"network_flow_log_more_than_90_days"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_4",
|
||||
"Name": "PI1.4 System outputs are complete, accurate, distributed only to intended parties, and retained to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to ensure system outputs are delivered to authorized recipients in the correct format and protected against unauthorized access, modification, theft, destruction, or corruption. This includes output encryption, access controls, and audit trails for output delivery.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_4",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "azure",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"storage_ensure_encryption_with_customer_managed_keys",
|
||||
"storage_infrastructure_encryption_is_enabled",
|
||||
"monitor_storage_account_with_activity_logs_cmk_encrypted",
|
||||
"monitor_storage_account_with_activity_logs_is_private",
|
||||
"sqlserver_tde_encryption_enabled",
|
||||
"sqlserver_tde_encrypted_with_cmk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_5",
|
||||
"Name": "PI1.5 Stored data is maintained complete, accurate, and protected from unauthorized modification to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to protect stored inputs, items in processing, and outputs from theft, destruction, corruption, or deterioration. This includes data encryption at rest, key management, backup and recovery procedures, access controls, and data integrity validation.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_5",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "azure",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"storage_ensure_encryption_with_customer_managed_keys",
|
||||
"storage_infrastructure_encryption_is_enabled",
|
||||
"storage_ensure_soft_delete_is_enabled",
|
||||
"vm_ensure_attached_disks_encrypted_with_cmk",
|
||||
"vm_ensure_unattached_disks_encrypted_with_cmk",
|
||||
"keyvault_key_rotation_enabled",
|
||||
"keyvault_recoverable"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,87 +492,6 @@
|
||||
"Checks": [
|
||||
"cloudstorage_bucket_log_retention_policy_lock"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_2",
|
||||
"Name": "PI1.2 System inputs are measured and recorded completely, accurately, and timely to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entity's objectives. This includes defining accuracy targets, monitoring input quality, and creating detailed records of each input event.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_2",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "gcp",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"compute_loadbalancer_logging_enabled",
|
||||
"compute_subnet_flow_logs_enabled",
|
||||
"logging_sink_created",
|
||||
"iam_audit_logs_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_3",
|
||||
"Name": "PI1.3 Data is processed completely, accurately, and timely as authorized to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to ensure data is processed completely, accurately, and timely. This includes defining processing specifications, identifying processing activities, detecting and correcting errors throughout processing, recording processing activities with accurate logs, and ensuring completeness and timeliness of processing.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_3",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "gcp",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
|
||||
"cloudsql_instance_postgres_log_connections_flag",
|
||||
"cloudsql_instance_postgres_log_disconnections_flag",
|
||||
"cloudsql_instance_postgres_log_statement_flag",
|
||||
"iam_audit_logs_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_4",
|
||||
"Name": "PI1.4 System outputs are complete, accurate, distributed only to intended parties, and retained to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to ensure system outputs are delivered to authorized recipients in the correct format and protected against unauthorized access, modification, theft, destruction, or corruption. This includes output encryption, access controls, and audit trails for output delivery.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_4",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "gcp",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudstorage_bucket_uniform_bucket_level_access",
|
||||
"bigquery_dataset_cmk_encryption",
|
||||
"bigquery_table_cmk_encryption",
|
||||
"compute_instance_confidential_computing_enabled",
|
||||
"pubsub_topic_encryption_with_cmk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "pi_1_5",
|
||||
"Name": "PI1.5 Stored data is maintained complete, accurate, and protected from unauthorized modification to meet the entity's processing integrity commitments and system requirements",
|
||||
"Description": "The entity implements controls to protect stored inputs, items in processing, and outputs from theft, destruction, corruption, or deterioration. This includes data encryption at rest, key management, backup and recovery procedures, access controls, and data integrity validation.",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "pi_1_5",
|
||||
"Section": "PI1.0 - Processing Integrity",
|
||||
"Service": "gcp",
|
||||
"Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudstorage_bucket_log_retention_policy_lock",
|
||||
"cloudsql_instance_automated_backups",
|
||||
"compute_instance_encryption_with_csek_enabled",
|
||||
"kms_key_rotation_enabled",
|
||||
"dataproc_encrypted_with_cmks_disabled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.16.1"
|
||||
prowler_version = "5.15.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"
|
||||
|
||||