Compare commits

..

3 Commits

Author SHA1 Message Date
Alan Buscaglia 9ad98ed47e fix(ui): code quality improvements for findings grouped view (#10515)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-03-31 17:07:50 +02:00
Alan Buscaglia 5306bb1133 fix(ui): security and crash fixes for findings grouped view (#10514) 2026-03-31 17:04:56 +02:00
Alan Buscaglia a7f18ec41f chore: init feature branch for findings groups improvements 2026-03-30 14:20:31 +02:00
409 changed files with 1410 additions and 20989 deletions
-7
View File
@@ -67,11 +67,6 @@ provider/googleworkspace:
- any-glob-to-any-file: "prowler/providers/googleworkspace/**"
- any-glob-to-any-file: "tests/providers/googleworkspace/**"
provider/vercel:
- changed-files:
- any-glob-to-any-file: "prowler/providers/vercel/**"
- any-glob-to-any-file: "tests/providers/vercel/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
@@ -107,8 +102,6 @@ mutelist:
- any-glob-to-any-file: "tests/providers/openstack/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/googleworkspace/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/googleworkspace/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/vercel/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/vercel/lib/mutelist/**"
integration/s3:
- changed-files:
-8
View File
@@ -177,14 +177,6 @@ modules:
- tests/providers/llm/**
e2e: []
- name: sdk-vercel
match:
- prowler/providers/vercel/**
- prowler/compliance/vercel/**
tests:
- tests/providers/vercel/**
e2e: []
# ============================================
# SDK - Lib modules
# ============================================
-51
View File
@@ -1,51 +0,0 @@
name: 'Tools: Lock Issue on Close'
on:
issues:
types:
- closed
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
lock:
if: |
github.repository == 'prowler-cloud/prowler' &&
github.event.issue.locked == false
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
- name: Comment and lock issue
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { owner, repo } = context.repo;
const issue_number = context.payload.issue.number;
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: 'This issue is now locked as it has been closed. If you are still hitting a related problem, please open a new issue and link back to this one for context. Thanks!'
});
} catch (error) {
core.warning(`Failed to post lock comment on issue #${issue_number}: ${error.message}`);
}
const lockParams = { owner, repo, issue_number };
if (context.payload.issue.state_reason === 'completed') {
lockParams.lock_reason = 'resolved';
}
await github.rest.issues.lock(lockParams);
-1
View File
@@ -76,7 +76,6 @@ jobs:
"StylusFrost"
"toniblyx"
"davidm4r"
"pfe-nazaries"
)
echo "Checking if $AUTHOR is a member of prowler-cloud organization"
@@ -1,180 +0,0 @@
name: 'Tools: Check Compliance Mapping'
on:
pull_request:
types:
- 'opened'
- 'synchronize'
- 'reopened'
- 'labeled'
- 'unlabeled'
branches:
- 'master'
- 'v5.*'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check-compliance-mapping:
if: contains(github.event.pull_request.labels.*.name, 'no-compliance-check') == false
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
github.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
prowler/providers/**/services/**/*.metadata.json
prowler/compliance/**/*.json
- name: Check if new checks are mapped in compliance
id: compliance-check
run: |
ADDED_METADATA="${STEPS_CHANGED_FILES_OUTPUTS_ADDED_FILES}"
ALL_CHANGED="${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}"
# Filter only new metadata files (new checks)
new_checks=""
for f in $ADDED_METADATA; do
case "$f" in *.metadata.json) new_checks="$new_checks $f" ;; esac
done
if [ -z "$(echo "$new_checks" | tr -d ' ')" ]; then
echo "No new checks detected."
echo "has_new_checks=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Collect compliance files changed in this PR
changed_compliance=""
for f in $ALL_CHANGED; do
case "$f" in prowler/compliance/*.json) changed_compliance="$changed_compliance $f" ;; esac
done
UNMAPPED=""
MAPPED=""
for metadata_file in $new_checks; do
check_dir=$(dirname "$metadata_file")
check_id=$(basename "$check_dir")
provider=$(echo "$metadata_file" | cut -d'/' -f3)
# Read CheckID from the metadata JSON for accuracy
if [ -f "$metadata_file" ]; then
json_check_id=$(python3 -c "import json; print(json.load(open('$metadata_file')).get('CheckID', ''))" 2>/dev/null || echo "")
if [ -n "$json_check_id" ]; then
check_id="$json_check_id"
fi
fi
# Search for the check ID in compliance files changed in this PR
found_in=""
for comp_file in $changed_compliance; do
if grep -q "\"${check_id}\"" "$comp_file" 2>/dev/null; then
found_in="${found_in}$(basename "$comp_file" .json), "
fi
done
if [ -n "$found_in" ]; then
found_in=$(echo "$found_in" | sed 's/, $//')
MAPPED="${MAPPED}- \`${check_id}\` (\`${provider}\`): ${found_in}"$'\n'
else
UNMAPPED="${UNMAPPED}- \`${check_id}\` (\`${provider}\`)"$'\n'
fi
done
echo "has_new_checks=true" >> "$GITHUB_OUTPUT"
if [ -n "$UNMAPPED" ]; then
echo "has_unmapped=true" >> "$GITHUB_OUTPUT"
else
echo "has_unmapped=false" >> "$GITHUB_OUTPUT"
fi
{
echo "unmapped<<EOF"
echo -e "${UNMAPPED}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "mapped<<EOF"
echo -e "${MAPPED}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
env:
STEPS_CHANGED_FILES_OUTPUTS_ADDED_FILES: ${{ steps.changed-files.outputs.added_files }}
STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
- name: Manage compliance review label
if: steps.compliance-check.outputs.has_new_checks == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HAS_UNMAPPED: ${{ steps.compliance-check.outputs.has_unmapped }}
run: |
LABEL_NAME="needs-compliance-review"
if [ "$HAS_UNMAPPED" = "true" ]; then
echo "Adding compliance review label to PR #${PR_NUMBER}..."
gh pr edit "$PR_NUMBER" --add-label "$LABEL_NAME" --repo "${{ github.repository }}" || true
else
echo "Removing compliance review label from PR #${PR_NUMBER}..."
gh pr edit "$PR_NUMBER" --remove-label "$LABEL_NAME" --repo "${{ github.repository }}" || true
fi
- name: Find existing compliance comment
if: steps.compliance-check.outputs.has_new_checks == 'true' && github.event.pull_request.head.repo.full_name == github.repository
id: find-comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- compliance-mapping-check -->'
- name: Create or update compliance comment
if: steps.compliance-check.outputs.has_new_checks == 'true' && github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!-- compliance-mapping-check -->
## Compliance Mapping Review
This PR adds new checks. Please verify that they have been mapped to the relevant compliance framework requirements.
${{ steps.compliance-check.outputs.unmapped != '' && format('### New checks not mapped to any compliance framework in this PR
{0}
> Please review whether these checks should be added to compliance framework requirements in `prowler/compliance/<provider>/`. Each compliance JSON has a `Checks` array inside each requirement — add the check ID there if it satisfies that requirement.', steps.compliance-check.outputs.unmapped) || '' }}
${{ steps.compliance-check.outputs.mapped != '' && format('### New checks already mapped in this PR
{0}', steps.compliance-check.outputs.mapped) || '' }}
Use the `no-compliance-check` label to skip this check.
+2 -26
View File
@@ -216,11 +216,11 @@ jobs:
echo "AWS service_paths='${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}'"
if [ "${STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL}" = "true" ]; then
poetry run pytest -p no:randomly -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
elif [ -z "${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}" ]; then
echo "No AWS service paths detected; skipping AWS tests."
else
poetry run pytest -p no:randomly -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}
fi
env:
STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL: ${{ steps.aws-services.outputs.run_all }}
@@ -499,30 +499,6 @@ jobs:
flags: prowler-py${{ matrix.python-version }}-googleworkspace
files: ./googleworkspace_coverage.xml
# Vercel Provider
- name: Check if Vercel files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-vercel
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/vercel/**
./tests/**/vercel/**
./poetry.lock
- name: Run Vercel tests
if: steps.changed-vercel.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/vercel --cov-report=xml:vercel_coverage.xml tests/providers/vercel
- name: Upload Vercel coverage to Codecov
if: steps.changed-vercel.outputs.any_changed == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler-py${{ matrix.python-version }}-vercel
files: ./vercel_coverage.xml
# Lib
- name: Check if Lib files changed
if: steps.check-changes.outputs.any_changed == 'true'
+1 -1
View File
@@ -165,7 +165,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
package_json_file: ui/package.json
version: 10
run_install: false
- name: Get pnpm store directory
+1 -1
View File
@@ -98,7 +98,7 @@ jobs:
if: steps.check-changes.outputs.any_changed == 'true'
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
package_json_file: ui/package.json
version: 10
run_install: false
- name: Get pnpm store directory
-1
View File
@@ -60,7 +60,6 @@ htmlcov/
**/mcp-config.json
**/mcpServers.json
.mcp/
.mcp.json
# AI Coding Assistants - Cursor
.cursorignore
+3 -22
View File
@@ -3,7 +3,7 @@
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png#gh-dark-mode-only" width="50%" height="50%">
</p>
<p align="center">
<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.
<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>
@@ -41,7 +41,7 @@
# Description
**Prowler** is the worlds 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 the worlds 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 includes hundreds of built-in controls to ensure compliance with standards and frameworks, including:
@@ -119,7 +119,6 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
| Image | N/A | N/A | N/A | N/A | Official | CLI, API |
| Google Workspace | 1 | 1 | 0 | 1 | Official | CLI |
| OpenStack | 27 | 4 | 0 | 8 | Official | UI, API, CLI |
| Vercel | 30 | 6 | 0 | 5 | Official | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
> [!Note]
@@ -240,21 +239,6 @@ pnpm start
> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started.
**Pre-commit Hooks Setup**
Some pre-commit hooks require tools installed on your system:
1. **Install [TruffleHog](https://github.com/trufflesecurity/trufflehog#install)** (secret scanning) — see the [official installation options](https://github.com/trufflesecurity/trufflehog#install).
2. **Install [Safety](https://github.com/pyupio/safety)** (dependency vulnerability checking):
```console
# Requires a Python environment (e.g. via pyenv)
pip install safety
```
3. **Install [Hadolint](https://github.com/hadolint/hadolint#install)** (Dockerfile linting) — see the [official installation options](https://github.com/hadolint/hadolint#install).
## Prowler CLI
### Pip package
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/). Consequently, it can be installed using pip with Python >=3.10, <3.13:
@@ -317,10 +301,7 @@ python prowler-cli.py -v
- **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.
![Prowler App Architecture](docs/images/products/prowler-app-architecture.png)
<!-- Diagram source: docs/images/products/prowler-app-architecture.mmd — edit there, re-render at https://mermaid.live, and replace the PNG. -->
![Prowler App Architecture](docs/products/img/prowler-app-architecture.png)
## Prowler CLI
-8
View File
@@ -6,11 +6,7 @@ All notable changes to the **Prowler API** are documented in this file.
### 🚀 Added
- Pin all unpinned dependencies to exact versions to prevent supply chain attacks and ensure reproducible builds [(#10469)](https://github.com/prowler-cloud/prowler/pull/10469)
- Filter RBAC role lookup by `tenant_id` to prevent cross-tenant privilege leak [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491)
- `VALKEY_SCHEME`, `VALKEY_USERNAME`, and `VALKEY_PASSWORD` environment variables to configure Celery broker TLS/auth connection details for Valkey/ElastiCache [(#10420)](https://github.com/prowler-cloud/prowler/pull/10420)
- `Vercel` provider support [(#10190)](https://github.com/prowler-cloud/prowler/pull/10190)
- Finding groups list and latest endpoints support `sort=delta`, ordering by `new_count` then `changed_count` so groups with the most new findings rank highest [(#10606)](https://github.com/prowler-cloud/prowler/pull/10606)
### 🔄 Changed
@@ -28,14 +24,10 @@ All notable changes to the **Prowler API** are documented in this file.
- Finding groups `check_title__icontains` resolution, `name__icontains` resource filter and `resource_group` field in `/resources` response [(#10486)](https://github.com/prowler-cloud/prowler/pull/10486)
- Membership `post_delete` signal using raw FK ids to avoid `DoesNotExist` during cascade deletions [(#10497)](https://github.com/prowler-cloud/prowler/pull/10497)
- Finding group resources endpoints returning false 404 when filters match no results, and `sort` parameter being ignored [(#10510)](https://github.com/prowler-cloud/prowler/pull/10510)
- Jira integration failing with `JiraInvalidIssueTypeError` on non-English Jira instances due to hardcoded `"Task"` issue type; now dynamically fetches available issue types per project [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534)
- Finding group `first_seen_at` now reflects when a new finding appeared in the scan instead of the oldest carry-forward date across all unchanged findings [(#10595)](https://github.com/prowler-cloud/prowler/pull/10595)
- Attack Paths: Remove `clear_cache` call from read-only query endpoints; cache clearing belongs to the scan/ingestion flow, not API queries [(#10586)](https://github.com/prowler-cloud/prowler/pull/10586)
### 🔐 Security
- Pin all unpinned dependencies to exact versions to prevent supply chain attacks and ensure reproducible builds [(#10469)](https://github.com/prowler-cloud/prowler/pull/10469)
- `authlib` bumped from 1.6.6 to 1.6.9 to fix CVE-2026-28802 (JWT `alg: none` validation bypass) [(#10579)](https://github.com/prowler-cloud/prowler/pull/10579)
---
+12 -12
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -943,14 +943,14 @@ files = [
[[package]]
name = "authlib"
version = "1.6.9"
version = "1.6.6"
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"},
{file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"},
{file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"},
{file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"},
]
[package.dependencies]
@@ -2961,7 +2961,7 @@ files = [
[package.dependencies]
autopep8 = "*"
Django = ">=4.2"
gprof2dot = ">=2017.09.19"
gprof2dot = ">=2017.9.19"
sqlparse = "*"
[[package]]
@@ -4569,7 +4569,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.03.6"
jsonschema-specifications = ">=2023.3.6"
referencing = ">=0.28.4"
rpds-py = ">=0.7.1"
@@ -4777,7 +4777,7 @@ librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""]
mongodb = ["pymongo (==4.15.3)"]
msgpack = ["msgpack (==1.1.2)"]
pyro = ["pyro4 (==4.82)"]
qpid = ["qpid-python (==1.36.0-1)", "qpid-tools (==1.36.0-1)"]
qpid = ["qpid-python (==1.36.0.post1)", "qpid-tools (==1.36.0.post1)"]
redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"]
slmq = ["softlayer_messaging (>=1.0.3)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
@@ -4798,7 +4798,7 @@ files = [
]
[package.dependencies]
certifi = ">=14.05.14"
certifi = ">=14.5.14"
durationpy = ">=0.7"
google-auth = ">=1.0.1"
oauthlib = ">=3.2.2"
@@ -6722,7 +6722,7 @@ uuid6 = "2024.7.10"
type = "git"
url = "https://github.com/prowler-cloud/prowler.git"
reference = "master"
resolved_reference = "6ac90eb1b58590b6f2f51645dbef17b9231053f4"
resolved_reference = "2ddd5b3091bcdd8c7d44aba73b13c5c6f8f99e35"
[[package]]
name = "psutil"
@@ -7161,7 +7161,7 @@ files = [
]
[package.dependencies]
astroid = ">=3.2.2,<=3.3.0-dev0"
astroid = ">=3.2.2,<=3.3.0.dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
@@ -8174,10 +8174,10 @@ files = [
]
[package.dependencies]
botocore = ">=1.37.4,<2.0a.0"
botocore = ">=1.37.4,<2.0a0"
[package.extras]
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
[[package]]
name = "safety"
+26 -17
View File
@@ -1,10 +1,10 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from rest_framework import permissions
from rest_framework.exceptions import NotAuthenticated
from rest_framework.filters import SearchFilter
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from rest_framework_json_api import filters
from rest_framework_json_api.views import ModelViewSet
@@ -12,7 +12,7 @@ from api.authentication import CombinedJWTOrAPIKeyAuthentication
from api.db_router import MainRouter, reset_read_db_alias, set_read_db_alias
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
from api.filters import CustomDjangoFilterBackend
from api.models import Role, UserRoleRelationship
from api.models import Role, Tenant
from api.rbac.permissions import HasPermissions
@@ -113,22 +113,27 @@ class BaseTenantViewset(BaseViewSet):
if request is not None:
request.db_alias = self.db_alias
if request.method == "POST":
with transaction.atomic(using=MainRouter.admin_db):
tenant = super().dispatch(request, *args, **kwargs)
if isinstance(tenant, Response) and tenant.status_code == 201:
self._create_admin_role(tenant.data["id"])
return tenant
else:
with transaction.atomic(using=self.db_alias):
return super().dispatch(request, *args, **kwargs)
with transaction.atomic(using=self.db_alias):
tenant = super().dispatch(request, *args, **kwargs)
try:
# If the request is a POST, create the admin role
if request.method == "POST":
isinstance(tenant, dict) and self._create_admin_role(
tenant.data["id"]
)
except Exception as e:
self._handle_creation_error(e, tenant)
raise
return tenant
finally:
if alias_token is not None:
reset_read_db_alias(alias_token)
self.db_alias = MainRouter.default_db
def _create_admin_role(self, tenant_id):
admin_role = Role.objects.using(MainRouter.admin_db).create(
Role.objects.using(MainRouter.admin_db).create(
name="admin",
tenant_id=tenant_id,
manage_users=True,
@@ -139,11 +144,15 @@ class BaseTenantViewset(BaseViewSet):
manage_scans=True,
unlimited_visibility=True,
)
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
user=self.request.user,
role=admin_role,
tenant_id=tenant_id,
)
def _handle_creation_error(self, error, tenant):
if tenant.data.get("id"):
try:
Tenant.objects.using(MainRouter.admin_db).filter(
id=tenant.data["id"]
).delete()
except ObjectDoesNotExist:
pass # Tenant might not exist, handle gracefully
def initial(self, request, *args, **kwargs):
if request.auth is None:
-1
View File
@@ -434,7 +434,6 @@ class ScanFilter(ProviderRelationshipFilterSet):
class Meta:
model = Scan
fields = {
"id": ["exact", "in"],
"provider": ["exact", "in"],
"name": ["exact", "icontains"],
"started_at": ["gte", "lte"],
@@ -1,40 +0,0 @@
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0086_attack_paths_cleanup_periodic_task"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("mongodbatlas", "MongoDB Atlas"),
("iac", "IaC"),
("oraclecloud", "Oracle Cloud Infrastructure"),
("alibabacloud", "Alibaba Cloud"),
("cloudflare", "Cloudflare"),
("openstack", "OpenStack"),
("image", "Image"),
("googleworkspace", "Google Workspace"),
("vercel", "Vercel"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'vercel';",
reverse_sql=migrations.RunSQL.noop,
),
]
+1 -11
View File
@@ -4,11 +4,11 @@ import re
from datetime import datetime, timedelta, timezone
from uuid import UUID, uuid4
import defusedxml
from allauth.socialaccount.models import SocialApp
from config.custom_logging import BackendLogger
from config.settings.social_login import SOCIALACCOUNT_PROVIDERS
from cryptography.fernet import Fernet, InvalidToken
import defusedxml
from defusedxml import ElementTree as ET
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
@@ -295,7 +295,6 @@ class Provider(RowLevelSecurityProtectedModel):
OPENSTACK = "openstack", _("OpenStack")
IMAGE = "image", _("Image")
GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace")
VERCEL = "vercel", _("Vercel")
@staticmethod
def validate_aws_uid(value):
@@ -439,15 +438,6 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_vercel_uid(value):
if not re.match(r"^team_[a-zA-Z0-9]{16,32}$", value):
raise ModelValidationError(
detail="Vercel provider ID must be a valid Vercel Team ID (e.g., team_xxxxxxxxxxxxxxxxxxxxxxxx).",
code="vercel-uid",
pointer="/data/attributes/uid",
)
@staticmethod
def validate_image_uid(value):
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{2,249}$", value):
+7 -16
View File
@@ -1,7 +1,7 @@
from enum import Enum
from typing import Optional
from django.db.models import QuerySet
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import BasePermission
from api.db_router import MainRouter
@@ -29,17 +29,11 @@ class HasPermissions(BasePermission):
if not required_permissions:
return True
tenant_id = getattr(request, "tenant_id", None)
if not tenant_id:
tenant_id = request.auth.get("tenant_id") if request.auth else None
if not tenant_id:
return False
user_roles = (
User.objects.using(MainRouter.admin_db)
.get(id=request.user.id)
.roles.using(MainRouter.admin_db)
.filter(tenant_id=tenant_id)
.all()
)
if not user_roles:
return False
@@ -51,17 +45,14 @@ class HasPermissions(BasePermission):
return True
def get_role(user: User, tenant_id: str) -> Role:
def get_role(user: User) -> Optional[Role]:
"""
Retrieve the role assigned to the given user in the specified tenant.
Retrieve the first role assigned to the given user.
Raises:
PermissionDenied: If the user has no role in the given tenant.
Returns:
The user's first Role instance if the user has any roles, otherwise None.
"""
role = user.roles.using(MainRouter.admin_db).filter(tenant_id=tenant_id).first()
if role is None:
raise PermissionDenied("User has no role in this tenant.")
return role
return user.roles.first()
def get_providers(role: Role) -> QuerySet[Provider]:
-132
View File
@@ -372,7 +372,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -388,7 +387,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -411,7 +409,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -429,7 +426,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -1355,7 +1351,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -1371,7 +1366,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -1833,7 +1827,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -1849,7 +1842,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -1872,7 +1864,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -1890,7 +1881,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -2439,7 +2429,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -2455,7 +2444,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -2478,7 +2466,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -2496,7 +2483,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -2953,7 +2939,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -2969,7 +2954,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -2992,7 +2976,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -3010,7 +2993,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -3465,7 +3447,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -3481,7 +3462,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -3504,7 +3484,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -3522,7 +3501,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -3965,7 +3943,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -3981,7 +3958,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -4004,7 +3980,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -4022,7 +3997,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -5806,7 +5780,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -5822,7 +5795,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -5845,7 +5817,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -5863,7 +5834,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- name: filter[search]
@@ -5985,7 +5955,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6001,7 +5970,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -6024,7 +5992,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -6042,7 +6009,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- name: filter[search]
@@ -6153,7 +6119,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6169,7 +6134,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -6191,7 +6155,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -6209,7 +6172,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- name: filter[search]
@@ -6352,7 +6314,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6368,7 +6329,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -6391,7 +6351,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -6409,7 +6368,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -6565,7 +6523,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6581,7 +6538,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -6604,7 +6560,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -6622,7 +6577,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -6772,7 +6726,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6788,7 +6741,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -6810,7 +6762,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -6828,7 +6779,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- name: filter[search]
@@ -7020,7 +6970,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7036,7 +6985,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -7059,7 +7007,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -7077,7 +7024,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -7198,7 +7144,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7214,7 +7159,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -7237,7 +7181,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -7255,7 +7198,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -7400,7 +7342,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7416,7 +7357,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -7439,7 +7379,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -7457,7 +7396,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -8243,7 +8181,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8259,7 +8196,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider__in]
schema:
@@ -8282,7 +8218,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -8300,7 +8235,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -8323,7 +8257,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8339,7 +8272,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -8362,7 +8294,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -8380,7 +8311,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- name: filter[search]
@@ -9050,7 +8980,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -9066,7 +8995,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -9089,7 +9017,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -9107,7 +9034,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -9601,7 +9527,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -9617,7 +9542,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -9640,7 +9564,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -9658,7 +9581,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -9965,7 +9887,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -9981,7 +9902,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -10004,7 +9924,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -10022,7 +9941,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -10335,7 +10253,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -10351,7 +10268,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -10374,7 +10290,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -10392,7 +10307,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -11215,7 +11129,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -11231,7 +11144,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
- in: query
name: filter[provider_type__in]
schema:
@@ -11254,7 +11166,6 @@ paths:
- mongodbatlas
- openstack
- oraclecloud
- vercel
description: |-
Multiple values may be separated by commas.
@@ -11272,7 +11183,6 @@ paths:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
explode: false
style: form
- in: query
@@ -18553,15 +18463,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Vercel API Token
properties:
api_token:
type: string
description: Vercel API token for authentication. Can be scoped
to a specific team.
required:
- api_token
writeOnly: true
required:
- secret
@@ -19564,7 +19465,6 @@ components:
- openstack
- image
- googleworkspace
- vercel
type: string
description: |-
* `aws` - AWS
@@ -19581,7 +19481,6 @@ components:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
x-spec-enum-id: c0d56cad8ab9abe5
uid:
type: string
@@ -19702,7 +19601,6 @@ components:
- openstack
- image
- googleworkspace
- vercel
type: string
x-spec-enum-id: c0d56cad8ab9abe5
description: |-
@@ -19722,7 +19620,6 @@ components:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
uid:
type: string
title: Unique identifier for the provider, set by the provider
@@ -19774,7 +19671,6 @@ components:
- openstack
- image
- googleworkspace
- vercel
type: string
x-spec-enum-id: c0d56cad8ab9abe5
description: |-
@@ -19794,7 +19690,6 @@ components:
* `openstack` - OpenStack
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
uid:
type: string
minLength: 3
@@ -20644,15 +20539,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Vercel API Token
properties:
api_token:
type: string
description: Vercel API token for authentication. Can be scoped
to a specific team.
required:
- api_token
writeOnly: true
required:
- secret_type
@@ -21069,15 +20955,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Vercel API Token
properties:
api_token:
type: string
description: Vercel API token for authentication. Can be scoped
to a specific team.
required:
- api_token
writeOnly: true
required:
- secret_type
@@ -21504,15 +21381,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Vercel API Token
properties:
api_token:
type: string
description: Vercel API token for authentication. Can be scoped
to a specific team.
required:
- api_token
writeOnly: true
required:
- secret
@@ -215,21 +215,6 @@ class TestTokenSwitchTenant:
tenant_id = tenants_fixture[0].id
user_instance = User.objects.get(email=test_user)
Membership.objects.create(user=user_instance, tenant_id=tenant_id)
# Assign an admin role in the target tenant so the user can access resources
target_role = Role.objects.create(
name="admin",
tenant_id=tenant_id,
manage_users=True,
manage_account=True,
manage_billing=True,
manage_providers=True,
manage_integrations=True,
manage_scans=True,
unlimited_visibility=True,
)
UserRoleRelationship.objects.create(
user=user_instance, role=target_role, tenant_id=tenant_id
)
# Check that using our new user's credentials we can authenticate and get the providers
access_token, _ = get_api_tokens(client, test_user, test_password)
+1 -64
View File
@@ -2,7 +2,7 @@ import json
from unittest.mock import ANY, Mock, patch
import pytest
from conftest import TEST_PASSWORD, TODAY
from conftest import TODAY
from django.urls import reverse
from rest_framework import status
@@ -830,66 +830,3 @@ class TestUserRoleLinkPermissions:
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.django_db
class TestCrossTenantRoleLeak:
"""Regression tests for get_role() cross-tenant privilege leak.
get_role() must query admin_db (bypassing RLS) so that a user with a role
in tenant A cannot accidentally pass role checks when authenticated against
tenant B where they have no role.
"""
def test_user_with_role_in_tenant_a_denied_in_tenant_b(self, tenants_fixture):
"""User has admin role in tenant A, membership in tenant B but no role.
Hitting an RBAC-protected endpoint with a tenant-B token must return 403."""
from rest_framework.test import APIClient
tenant_a = tenants_fixture[0]
tenant_b = tenants_fixture[1]
user = User.objects.create_user(
name="cross_tenant_user",
email="cross_tenant@test.com",
password=TEST_PASSWORD,
)
Membership.objects.create(
user=user, tenant=tenant_a, role=Membership.RoleChoices.OWNER
)
Membership.objects.create(
user=user, tenant=tenant_b, role=Membership.RoleChoices.OWNER
)
# Role only in tenant A
role = Role.objects.create(
name="admin",
tenant_id=tenant_a.id,
manage_users=True,
manage_account=True,
manage_billing=True,
manage_providers=True,
manage_integrations=True,
manage_scans=True,
unlimited_visibility=True,
)
UserRoleRelationship.objects.create(user=user, role=role, tenant_id=tenant_a.id)
# Mint token scoped to tenant B (where user has NO role)
serializer = TokenSerializer(
data={
"type": "tokens",
"email": "cross_tenant@test.com",
"password": TEST_PASSWORD,
"tenant_id": tenant_b.id,
}
)
serializer.is_valid(raise_exception=True)
access_token = serializer.validated_data["access"]
client = APIClient()
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
# user-list requires manage_users permission via HasPermissions
response = client.get(reverse("user-list"))
assert response.status_code == status.HTTP_403_FORBIDDEN
+5 -50
View File
@@ -33,7 +33,6 @@ from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider
from prowler.providers.openstack.openstack_provider import OpenstackProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
from prowler.providers.vercel.vercel_provider import VercelProvider
class TestMergeDicts:
@@ -129,7 +128,6 @@ class TestReturnProwlerProvider:
(Provider.ProviderChoices.CLOUDFLARE.value, CloudflareProvider),
(Provider.ProviderChoices.OPENSTACK.value, OpenstackProvider),
(Provider.ProviderChoices.IMAGE.value, ImageProvider),
(Provider.ProviderChoices.VERCEL.value, VercelProvider),
],
)
def test_return_prowler_provider(self, provider_type, expected_provider):
@@ -220,24 +218,6 @@ class TestProwlerProviderConnectionTest:
registry_token="tok123",
)
@patch("api.utils.return_prowler_provider")
def test_prowler_provider_connection_test_vercel_provider(
self, mock_return_prowler_provider
):
"""Test connection test for Vercel provider passes team_id."""
provider = MagicMock()
provider.uid = "team_abcdef1234567890"
provider.provider = Provider.ProviderChoices.VERCEL.value
provider.secret.secret = {"api_token": "vercel_token_123"}
mock_return_prowler_provider.return_value = MagicMock()
prowler_provider_connection_test(provider)
mock_return_prowler_provider.return_value.test_connection.assert_called_once_with(
api_token="vercel_token_123",
team_id="team_abcdef1234567890",
raise_on_exception=False,
)
@patch("api.utils.return_prowler_provider")
def test_prowler_provider_connection_test_image_provider_no_creds(
self, mock_return_prowler_provider
@@ -304,10 +284,6 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.OPENSTACK.value,
{},
),
(
Provider.ProviderChoices.VERCEL.value,
{"team_id": "provider_uid"},
),
],
)
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
@@ -806,15 +782,11 @@ class TestProwlerIntegrationConnectionTest:
}
integration.configuration = {}
# Mock successful JIRA connection with projects and issue types
# Mock successful JIRA connection with projects
mock_connection = MagicMock()
mock_connection.is_connected = True
mock_connection.error = None
mock_connection.projects = {"PROJ1": "Project 1", "PROJ2": "Project 2"}
mock_connection.issue_types = {
"PROJ1": ["Task", "Bug"],
"PROJ2": ["Task", "Story"],
}
mock_jira_class.test_connection.return_value = mock_connection
# Mock rls_transaction context manager
@@ -843,12 +815,6 @@ class TestProwlerIntegrationConnectionTest:
"PROJ2": "Project 2",
}
# Verify issue types were saved to integration configuration
assert integration.configuration["issue_types"] == {
"PROJ1": ["Task", "Bug"],
"PROJ2": ["Task", "Story"],
}
# Verify integration.save() was called
integration.save.assert_called_once()
@@ -872,7 +838,6 @@ class TestProwlerIntegrationConnectionTest:
mock_connection.is_connected = False
mock_connection.error = Exception("Authentication failed: Invalid credentials")
mock_connection.projects = {} # Empty projects when connection fails
mock_connection.issue_types = {} # Empty issue types when connection fails
mock_jira_class.test_connection.return_value = mock_connection
# Mock rls_transaction context manager
@@ -898,9 +863,6 @@ class TestProwlerIntegrationConnectionTest:
# Verify empty projects dict was saved to integration configuration
assert integration.configuration["projects"] == {}
# Verify empty issue types dict was saved to integration configuration
assert integration.configuration["issue_types"] == {}
# Verify integration.save() was called even on connection failure
integration.save.assert_called_once()
@@ -919,11 +881,11 @@ class TestProwlerIntegrationConnectionTest:
"domain": "example.atlassian.net",
}
integration.configuration = {
"issue_types": {"OLD_PROJ": ["Task"]}, # Existing configuration
"issue_types": ["Task"], # Existing configuration
"projects": {"OLD_PROJ": "Old Project"}, # Will be overwritten
}
# Mock successful JIRA connection with new projects and issue types
# Mock successful JIRA connection with new projects
mock_connection = MagicMock()
mock_connection.is_connected = True
mock_connection.error = None
@@ -931,10 +893,6 @@ class TestProwlerIntegrationConnectionTest:
"NEW_PROJ1": "New Project 1",
"NEW_PROJ2": "New Project 2",
}
mock_connection.issue_types = {
"NEW_PROJ1": ["Task", "Bug"],
"NEW_PROJ2": ["Story"],
}
mock_jira_class.test_connection.return_value = mock_connection
# Mock rls_transaction context manager
@@ -952,11 +910,8 @@ class TestProwlerIntegrationConnectionTest:
"NEW_PROJ2": "New Project 2",
}
# Verify issue types were also updated
assert integration.configuration["issue_types"] == {
"NEW_PROJ1": ["Task", "Bug"],
"NEW_PROJ2": ["Story"],
}
# Verify other configuration fields were preserved
assert integration.configuration["issue_types"] == ["Task"]
# Verify integration.save() was called
integration.save.assert_called_once()
+22 -190
View File
@@ -516,13 +516,6 @@ class TestTenantViewSet:
response.json()["data"]["attributes"]["name"]
== valid_tenant_payload["name"]
)
new_tenant_id = response.json()["data"]["id"]
user = authenticated_client.user
assert UserRoleRelationship.objects.filter(
user=user,
tenant_id=new_tenant_id,
role__name="admin",
).exists()
def test_tenants_invalid_create(self, authenticated_client, invalid_tenant_payload):
response = authenticated_client.post(
@@ -582,66 +575,22 @@ class TestTenantViewSet:
Tenant.objects.filter(pk=kwargs.get("tenant_id")).delete()
delete_tenant_mock.side_effect = _delete_tenant
# Use tenant2 where the user is OWNER
_, tenant2, _ = tenants_fixture
response = authenticated_client.delete(
reverse("tenant-detail", kwargs={"pk": tenant2.id})
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert Membership.objects.filter(tenant_id=tenant2.id).count() == 0
# User is not deleted because it has another membership (tenant1)
assert User.objects.count() == 1
@patch("api.v1.views.delete_tenant_task.apply_async")
def test_tenants_delete_as_member_forbidden(
self, delete_tenant_mock, authenticated_client, tenants_fixture
):
# tenant1: user is MEMBER, not OWNER -> should be forbidden
tenant1, *_ = tenants_fixture
response = authenticated_client.delete(
reverse("tenant-detail", kwargs={"pk": tenant1.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
delete_tenant_mock.assert_not_called()
@patch("api.v1.views.delete_tenant_task.apply_async")
def test_tenants_delete_cross_tenant(
self, delete_tenant_mock, authenticated_client, tenants_fixture
):
# tenant3: user has no membership -> should be 404
_, _, tenant3 = tenants_fixture
response = authenticated_client.delete(
reverse("tenant-detail", kwargs={"pk": tenant3.id})
)
assert response.status_code == status.HTTP_404_NOT_FOUND
delete_tenant_mock.assert_not_called()
@patch("api.v1.views.delete_tenant_task.apply_async")
def test_tenants_delete_only_removes_exclusive_users(
self, delete_tenant_mock, authenticated_client, tenants_fixture, extra_users
):
def _delete_tenant(kwargs):
Tenant.objects.filter(pk=kwargs.get("tenant_id")).delete()
delete_tenant_mock.side_effect = _delete_tenant
_, tenant2, _ = tenants_fixture
# extra_users adds user2 (OWNER in tenant2) and user3 (MEMBER in tenant2)
# user2 and user3 are ONLY in tenant2, so they should be deleted
# The test user is in tenant1 + tenant2, so should NOT be deleted
initial_user_count = User.objects.count() # test_user + user2 + user3 = 3
assert initial_user_count == 3
response = authenticated_client.delete(
reverse("tenant-detail", kwargs={"pk": tenant2.id})
)
assert response.status_code == status.HTTP_204_NO_CONTENT
# user2 and user3 are deleted (no other memberships), test_user remains
assert Tenant.objects.count() == len(tenants_fixture) - 1
assert Membership.objects.filter(tenant_id=tenant1.id).count() == 0
# User is not deleted because it has another membership
assert User.objects.count() == 1
def test_tenants_delete_invalid(self, authenticated_client):
response = authenticated_client.delete(
reverse("tenant-detail", kwargs={"pk": "random_id"})
)
# To change if we implement RBAC
# (user might not have permissions to see if the tenant exists or not -> 200 empty)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_tenants_list_filter_search(self, authenticated_client, tenants_fixture):
@@ -745,6 +694,7 @@ class TestTenantViewSet:
# Test user + 2 extra users for tenant 2
assert len(response.json()["data"]) == 3
@patch("api.v1.views.TenantMembersViewSet.required_permissions", [])
def test_tenants_list_memberships_as_member(
self, authenticated_client, tenants_fixture, extra_users
):
@@ -857,30 +807,6 @@ class TestTenantViewSet:
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_tenants_delete_membership_cross_tenant(
self, authenticated_client, tenants_fixture
):
# Create a tenant with a different user's membership
other_tenant = Tenant.objects.create(name="Other Tenant")
other_user = User.objects.create_user(
name="other", password=TEST_PASSWORD, email="other@test.com"
)
other_membership = Membership.objects.create(
user=other_user,
tenant=other_tenant,
role=Membership.RoleChoices.OWNER,
)
# Authenticated user is NOT a member of other_tenant -> 404
response = authenticated_client.delete(
reverse(
"tenant-membership-detail",
kwargs={"tenant_pk": other_tenant.id, "pk": other_membership.id},
)
)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert Membership.objects.filter(id=other_membership.id).exists()
def test_tenants_list_no_permissions(
self, authenticated_client_no_permissions_rbac, tenants_fixture
):
@@ -1789,46 +1715,6 @@ class TestProviderViewSet:
"min_length",
"uid",
),
# Vercel UID validation - missing team_ prefix
(
{
"provider": "vercel",
"uid": "abcdef1234567890abcdef12",
"alias": "test",
},
"vercel-uid",
"uid",
),
# Vercel UID validation - too short after prefix
(
{
"provider": "vercel",
"uid": "team_abc123",
"alias": "test",
},
"vercel-uid",
"uid",
),
# Vercel UID validation - contains special characters
(
{
"provider": "vercel",
"uid": "team_abcdef-1234567890ab",
"alias": "test",
},
"vercel-uid",
"uid",
),
# Vercel UID validation - too long (33 chars after prefix)
(
{
"provider": "vercel",
"uid": "team_abcdefghijklmnopqrstuvwxyz1234567",
"alias": "test",
},
"vercel-uid",
"uid",
),
# Google Workspace UID validation - missing 'C' prefix
(
{
@@ -2032,21 +1918,21 @@ class TestProviderViewSet:
(
"uid.icontains",
"1",
12,
11,
),
("alias", "aws_testing_1", 1),
("alias.icontains", "aws", 2),
("inserted_at", TODAY, 13),
("inserted_at", TODAY, 12),
(
"inserted_at.gte",
"2024-01-01",
13,
12,
),
("inserted_at.lte", "2024-01-01", 0),
(
"updated_at.gte",
"2024-01-01",
13,
12,
),
("updated_at.lte", "2024-01-01", 0),
]
@@ -2671,14 +2557,6 @@ class TestProviderSecretViewSet:
"delegated_user": "admin@example.com",
},
),
# Vercel with API Token
(
Provider.ProviderChoices.VERCEL.value,
ProviderSecret.TypeChoices.STATIC,
{
"api_token": "fake-vercel-api-token-for-testing",
},
),
],
)
def test_provider_secrets_create_valid(
@@ -3357,29 +3235,6 @@ class TestScanViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 3
def test_scan_filter_by_id_exact(self, authenticated_client, scans_fixture):
scan1, *_ = scans_fixture
response = authenticated_client.get(
reverse("scan-list"),
{"filter[id]": str(scan1.id)},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
assert data[0]["id"] == str(scan1.id)
def test_scan_filter_by_id_in(self, authenticated_client, scans_fixture):
scan1, scan2, *_ = scans_fixture
response = authenticated_client.get(
reverse("scan-list"),
{"filter[id.in]": f"{scan1.id},{scan2.id}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 2
returned_ids = {item["id"] for item in data}
assert returned_ids == {str(scan1.id), str(scan2.id)}
def test_scans_filter_state_failed(self, authenticated_client, scans_fixture):
"""Ensure state filter matches only FAILED scans."""
scan1, *_ = scans_fixture
@@ -4287,6 +4142,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.attack_paths_views_helpers.execute_query",
return_value=graph_payload,
) as mock_execute,
patch("api.v1.views.graph_database.clear_cache") as mock_clear_cache,
):
response = authenticated_client.post(
reverse(
@@ -4313,6 +4169,7 @@ class TestAttackPathsScanViewSet:
prepared_parameters,
provider_id,
)
mock_clear_cache.assert_called_once_with(expected_db_name)
result = response.json()["data"]
attributes = result["attributes"]
assert attributes["nodes"] == graph_payload["nodes"]
@@ -4367,6 +4224,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.attack_paths_views_helpers.execute_query",
return_value=graph_payload,
),
patch("api.v1.views.graph_database.clear_cache"),
):
response = authenticated_client.post(
reverse(
@@ -4450,6 +4308,7 @@ class TestAttackPathsScanViewSet:
"truncated": False,
},
),
patch("api.v1.views.graph_database.clear_cache"),
patch(
"api.v1.views.graph_database.get_database_name", return_value="db-test"
),
@@ -4504,6 +4363,7 @@ class TestAttackPathsScanViewSet:
"truncated": False,
},
),
patch("api.v1.views.graph_database.clear_cache"),
patch(
"api.v1.views.graph_database.get_database_name", return_value="db-test"
),
@@ -4583,6 +4443,7 @@ class TestAttackPathsScanViewSet:
"truncated": False,
},
),
patch("api.v1.views.graph_database.clear_cache"),
):
response = authenticated_client.post(
reverse(
@@ -4648,6 +4509,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
patch("api.v1.views.graph_database.clear_cache"),
):
response = authenticated_client.post(
reverse(
@@ -4704,6 +4566,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
patch("api.v1.views.graph_database.clear_cache"),
):
response = authenticated_client.post(
reverse(
@@ -4750,6 +4613,7 @@ class TestAttackPathsScanViewSet:
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
patch("api.v1.views.graph_database.clear_cache"),
):
response = authenticated_client.post(
reverse(
@@ -5100,6 +4964,9 @@ class TestAttackPathsScanViewSet:
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
patch(
"api.v1.views.graph_database.clear_cache",
),
):
for i in range(11):
response = authenticated_client.post(
@@ -8230,8 +8097,6 @@ class TestUserRoleRelationshipViewSet:
manage_scans=False,
unlimited_visibility=False,
)
# Assign the role to the user
UserRoleRelationship.objects.create(user=user, role=only_role, tenant=tenant)
# Switch token to this tenant
serializer = TokenSerializer(
@@ -16839,39 +16704,6 @@ class TestFindingGroupViewSet:
data = response.json()["data"]
assert len(data) > 0
@pytest.mark.parametrize(
"endpoint_name", ["finding-group-list", "finding-group-latest"]
)
def test_finding_groups_sort_by_delta(
self,
authenticated_client,
finding_groups_fixture,
endpoint_name,
):
"""Sort by delta orders by new_count then changed_count (lexicographic)."""
params = {"sort": "-delta"}
if endpoint_name == "finding-group-list":
params["filter[inserted_at]"] = TODAY
response = authenticated_client.get(reverse(endpoint_name), params)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) > 0
def delta_key(item):
attrs = item["attributes"]
return (attrs.get("new_count", 0), attrs.get("changed_count", 0))
desc_keys = [delta_key(item) for item in data]
assert desc_keys == sorted(desc_keys, reverse=True)
# Ascending order produces the inverse arrangement
params["sort"] = "delta"
response = authenticated_client.get(reverse(endpoint_name), params)
assert response.status_code == status.HTTP_200_OK
asc_keys = [delta_key(item) for item in response.json()["data"]]
assert asc_keys == sorted(asc_keys)
def test_finding_groups_latest_ignores_date_filters(
self, authenticated_client, finding_groups_fixture
):
-23
View File
@@ -39,7 +39,6 @@ if TYPE_CHECKING:
)
from prowler.providers.openstack.openstack_provider import OpenstackProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
from prowler.providers.vercel.vercel_provider import VercelProvider
class CustomOAuth2Client(OAuth2Client):
@@ -95,7 +94,6 @@ def return_prowler_provider(
| MongodbatlasProvider
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
):
"""Return the Prowler provider class based on the given provider type.
@@ -177,10 +175,6 @@ def return_prowler_provider(
from prowler.providers.image.image_provider import ImageProvider
prowler_provider = ImageProvider
case Provider.ProviderChoices.VERCEL.value:
from prowler.providers.vercel.vercel_provider import VercelProvider
prowler_provider = VercelProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
return prowler_provider
@@ -241,11 +235,6 @@ def get_prowler_provider_kwargs(
# clouds_yaml_content, clouds_yaml_cloud and provider_id are validated
# in the provider itself, so it's not needed here.
pass
elif provider.provider == Provider.ProviderChoices.VERCEL.value:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"team_id": provider.uid,
}
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
# Detect whether uid is a registry URL (e.g. "docker.io/andoniaf") or
# a concrete image reference (e.g. "docker.io/andoniaf/myimage:latest").
@@ -292,7 +281,6 @@ def initialize_prowler_provider(
| MongodbatlasProvider
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
):
"""Initialize a Prowler provider instance based on the given provider type.
@@ -344,13 +332,6 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
"raise_on_exception": False,
}
return prowler_provider.test_connection(**openstack_kwargs)
elif provider.provider == Provider.ProviderChoices.VERCEL.value:
vercel_kwargs = {
**prowler_provider_kwargs,
"team_id": provider.uid,
"raise_on_exception": False,
}
return prowler_provider.test_connection(**vercel_kwargs)
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
image_kwargs = {
"image": provider.uid,
@@ -434,12 +415,8 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
raise_on_exception=False,
)
project_keys = jira_connection.projects if jira_connection.is_connected else {}
issue_types = (
jira_connection.issue_types if jira_connection.is_connected else {}
)
with rls_transaction(str(integration.tenant_id)):
integration.configuration["projects"] = project_keys
integration.configuration["issue_types"] = issue_types
integration.save()
return jira_connection
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
@@ -69,10 +69,8 @@ class SecurityHubConfigSerializer(BaseValidateSerializer):
class JiraConfigSerializer(BaseValidateSerializer):
domain = serializers.CharField(read_only=True)
issue_types = serializers.DictField(
read_only=True,
child=serializers.ListField(child=serializers.CharField()),
default={},
issue_types = serializers.ListField(
read_only=True, child=serializers.CharField(), default=["Task"]
)
projects = serializers.DictField(read_only=True)
@@ -404,17 +404,6 @@ from rest_framework_json_api import serializers
},
"required": ["clouds_yaml_content", "clouds_yaml_cloud"],
},
{
"type": "object",
"title": "Vercel API Token",
"properties": {
"api_token": {
"type": "string",
"description": "Vercel API token for authentication. Can be scoped to a specific team.",
},
},
"required": ["api_token"],
},
]
}
)
+3 -42
View File
@@ -1573,8 +1573,6 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = OpenStackCloudsYamlProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.IMAGE.value:
serializer = ImageProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.VERCEL.value:
serializer = VercelProviderSecret(data=secret)
else:
raise serializers.ValidationError(
{"provider": f"Provider type not supported {provider_type}"}
@@ -1781,13 +1779,6 @@ class ImageProviderSecret(serializers.Serializer):
return attrs
class VercelProviderSecret(serializers.Serializer):
api_token = serializers.CharField()
class Meta:
resource_name = "provider-secrets"
class AlibabaCloudProviderSecret(serializers.Serializer):
access_key_id = serializers.CharField()
access_key_secret = serializers.CharField()
@@ -2722,11 +2713,11 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
)
config_serializer = JiraConfigSerializer
# Create non-editable configuration for JIRA integration
# issue_types will be populated per project when connection is tested
default_jira_issue_types = ["Task"]
configuration.update(
{
"projects": {},
"issue_types": {},
"issue_types": default_jira_issue_types,
"domain": credentials.get("domain"),
}
)
@@ -2941,25 +2932,13 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
return representation
class IntegrationJiraIssueTypesSerializer(BaseSerializerV1):
"""
Serializer for Jira issue types response.
"""
project_key = serializers.CharField(read_only=True)
issue_types = serializers.ListField(child=serializers.CharField(), read_only=True)
class JSONAPIMeta:
resource_name = "jira-issue-types"
class IntegrationJiraDispatchSerializer(BaseSerializerV1):
"""
Serializer for dispatching findings to JIRA integration.
"""
project_key = serializers.CharField(required=True)
issue_type = serializers.CharField(required=True)
issue_type = serializers.ChoiceField(required=True, choices=["Task"])
class JSONAPIMeta:
resource_name = "integrations-jira-dispatches"
@@ -2988,23 +2967,6 @@ class IntegrationJiraDispatchSerializer(BaseSerializerV1):
}
)
issue_type = attrs.get("issue_type")
available_issue_types = integration_instance.configuration.get(
"issue_types", {}
)
# Handle old format where issue_types was a flat list (e.g., ["Task"])
if not isinstance(available_issue_types, dict):
available_issue_types = {}
project_issue_types = available_issue_types.get(project_key, [])
if project_issue_types and issue_type not in project_issue_types:
raise ValidationError(
{
"issue_type": f"The issue type '{issue_type}' is not available for project '{project_key}'. "
f"Available types: {', '.join(project_issue_types)}. "
"Refresh the connection if this is an error."
}
)
return validated_attrs
@@ -4216,7 +4178,6 @@ class FindingGroupResourceSerializer(BaseSerializerV1):
provider = serializers.SerializerMethodField()
status = serializers.CharField()
severity = serializers.CharField()
delta = serializers.CharField(required=False, allow_null=True)
first_seen_at = serializers.DateTimeField(required=False, allow_null=True)
last_seen_at = serializers.DateTimeField(required=False, allow_null=True)
muted_reason = serializers.CharField(required=False, allow_null=True)
+36 -204
View File
@@ -207,7 +207,6 @@ from api.rls import Tenant
from api.utils import (
CustomOAuth2Client,
get_findings_metadata_no_aggregations,
initialize_prowler_integration,
initialize_prowler_provider,
validate_invitation,
)
@@ -236,7 +235,6 @@ from api.v1.serializers import (
FindingsSeverityOverTimeSerializer,
IntegrationCreateSerializer,
IntegrationJiraDispatchSerializer,
IntegrationJiraIssueTypesSerializer,
IntegrationSerializer,
IntegrationUpdateSerializer,
InvitationAcceptSerializer,
@@ -949,12 +947,7 @@ class UserViewSet(BaseUserViewset):
def get_serializer_context(self):
context = super().get_serializer_context()
if self.request.user.is_authenticated:
tenant_id = getattr(self.request, "tenant_id", None)
if tenant_id:
try:
context["role"] = get_role(self.request.user, tenant_id)
except PermissionDenied:
context["role"] = None
context["role"] = get_role(self.request.user)
return context
@action(detail=False, methods=["get"], url_name="me")
@@ -1236,44 +1229,28 @@ class TenantViewSet(BaseTenantViewset):
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
tenant = Tenant.objects.using(MainRouter.admin_db).create(
**serializer.validated_data
)
Membership.objects.using(MainRouter.admin_db).create(
tenant = serializer.save()
Membership.objects.create(
user=self.request.user, tenant=tenant, role=Membership.RoleChoices.OWNER
)
serializer.instance = tenant
return Response(data=serializer.data, status=status.HTTP_201_CREATED)
def destroy(self, request, *args, **kwargs):
tenant = self.get_object()
tenant_id = str(tenant.id)
# Only owners can delete a tenant
membership = Membership.objects.filter(user=request.user, tenant=tenant).first()
if not membership or membership.role != Membership.RoleChoices.OWNER:
raise PermissionDenied("Only owners can delete a tenant.")
# This will perform validation and raise a 404 if the tenant does not exist
tenant_id = kwargs.get("pk")
get_object_or_404(Tenant, id=tenant_id)
with transaction.atomic():
# Collect user IDs from this tenant's memberships before deleting them
tenant_user_ids = set(
Membership.objects.using(MainRouter.admin_db)
.filter(tenant_id=tenant_id)
.values_list("user_id", flat=True)
)
# Delete memberships for this tenant
# Delete memberships
Membership.objects.using(MainRouter.admin_db).filter(
tenant_id=tenant_id
).delete()
# Delete only users that were exclusively in this tenant
if tenant_user_ids:
User.objects.using(MainRouter.admin_db).filter(
id__in=tenant_user_ids, membership__isnull=True
).delete()
# Delete tenant data in background
# Delete users without memberships
User.objects.using(MainRouter.admin_db).filter(
membership__isnull=True
).delete()
# Delete tenant in batches
delete_tenant_task.apply_async(kwargs={"tenant_id": tenant_id})
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1338,12 +1315,8 @@ class TenantMembersViewSet(BaseTenantViewset):
http_method_names = ["get", "delete"]
serializer_class = MembershipSerializer
queryset = Membership.objects.none()
# Authorization is handled by get_requesting_membership (owner/member checks),
# not by RBAC, since the target tenant differs from the JWT tenant.
required_permissions = []
def set_required_permissions(self):
self.required_permissions = []
# RBAC required permissions
required_permissions = [Permissions.MANAGE_ACCOUNT]
def get_queryset(self):
tenant = self.get_tenant()
@@ -1356,10 +1329,8 @@ class TenantMembersViewSet(BaseTenantViewset):
def get_tenant(self):
tenant_id = self.kwargs.get("tenant_pk")
return get_object_or_404(
Tenant.objects.filter(membership__user=self.request.user),
id=tenant_id,
)
tenant = get_object_or_404(Tenant, id=tenant_id)
return tenant
def get_requesting_membership(self, tenant):
try:
@@ -1446,7 +1417,7 @@ class ProviderGroupViewSet(BaseRLSViewSet):
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
# Check if any of the user's roles have UNLIMITED_VISIBILITY
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all provider groups
@@ -1615,7 +1586,7 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all providers
queryset = Provider.objects.filter(tenant_id=self.request.tenant_id)
@@ -1870,7 +1841,7 @@ class ScanViewSet(BaseRLSViewSet):
self.required_permissions = [Permissions.MANAGE_SCANS]
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all scans
queryset = Scan.objects.filter(tenant_id=self.request.tenant_id)
@@ -2521,7 +2492,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
return super().get_serializer_class()
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
base_queryset = AttackPathsScan.objects.filter(tenant_id=self.request.tenant_id)
if user_roles.unlimited_visibility:
@@ -2628,6 +2599,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
provider_id,
)
query_duration = time.monotonic() - start
graph_database.clear_cache(database_name)
result_nodes = len(graph.get("nodes", []))
result_relationships = len(graph.get("relationships", []))
@@ -2695,6 +2667,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
provider_id,
)
query_duration = time.monotonic() - start
graph_database.clear_cache(database_name)
query_length = len(serializer.validated_data["query"])
result_nodes = len(graph.get("nodes", []))
@@ -2856,7 +2829,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
required_permissions = []
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all scans
queryset = Resource.all_objects.filter(tenant_id=self.request.tenant_id)
@@ -3478,7 +3451,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
def get_queryset(self):
tenant_id = self.request.tenant_id
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all findings
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
@@ -4081,9 +4054,9 @@ class RoleViewSet(BaseRLSViewSet):
)
)
def partial_update(self, request, *args, **kwargs):
user_role = get_role(request.user, request.tenant_id)
user_role = get_role(request.user)
# If the user is the owner of the role, the manage_account field is not editable
if kwargs["pk"] == str(user_role.id):
if user_role and kwargs["pk"] == str(user_role.id):
request.data["manage_account"] = str(user_role.manage_account).lower()
return super().partial_update(request, *args, **kwargs)
@@ -4339,7 +4312,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
required_permissions = []
def get_queryset(self):
role = get_role(self.request.user, self.request.tenant_id)
role = get_role(self.request.user)
unlimited_visibility = getattr(
role, Permissions.UNLIMITED_VISIBILITY.value, False
)
@@ -4381,7 +4354,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
def _compliance_summaries_queryset(self, scan_id):
"""Return pre-aggregated summaries constrained by RBAC visibility."""
role = get_role(self.request.user, self.request.tenant_id)
role = get_role(self.request.user)
unlimited_visibility = getattr(
role, Permissions.UNLIMITED_VISIBILITY.value, False
)
@@ -4923,7 +4896,7 @@ class OverviewViewSet(BaseRLSViewSet):
required_permissions = []
def get_queryset(self):
role = get_role(self.request.user, self.request.tenant_id)
role = get_role(self.request.user)
providers = get_providers(role)
if not role.unlimited_visibility:
@@ -6096,7 +6069,7 @@ class IntegrationViewSet(BaseRLSViewSet):
allowed_providers = None
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all integrations
queryset = Integration.objects.filter(tenant_id=self.request.tenant_id)
@@ -6151,15 +6124,7 @@ class IntegrationViewSet(BaseRLSViewSet):
tags=["Integration"],
summary="Send findings to a Jira integration",
description="Send a set of filtered findings to the given integration. At least one finding filter must be "
"provided.\n\n"
"## Known Limitations\n\n"
"### Issue Types with Required Custom Fields\n\n"
"Certain Jira issue types (such as Epic) may require mandatory custom fields that Prowler does not "
"currently populate when creating work items. If a selected issue type enforces required fields beyond "
'the standard set (e.g., "Team", "Epic Name"), the work item creation will fail.\n\n'
"To avoid this, select an issue type that does not require additional custom fields - **Task**, **Bug**, "
"or **Story** typically work without restrictions. If unsure which issue types are available for a project, "
'Prowler automatically fetches and displays them in the "Issue Type" selector when sending a finding.',
"provided.",
responses={202: OpenApiResponse(response=TaskSerializer)},
filters=True,
)
@@ -6167,7 +6132,7 @@ class IntegrationViewSet(BaseRLSViewSet):
class IntegrationJiraViewSet(BaseRLSViewSet):
queryset = Finding.all_objects.all()
serializer_class = IntegrationJiraDispatchSerializer
http_method_names = ["get", "post"]
http_method_names = ["post"]
filter_backends = [CustomDjangoFilterBackend]
filterset_class = IntegrationJiraFindingsFilter
# RBAC required permissions
@@ -6177,27 +6142,9 @@ class IntegrationJiraViewSet(BaseRLSViewSet):
def create(self, request, *args, **kwargs):
raise MethodNotAllowed(method="POST")
@extend_schema(exclude=True)
def list(self, request, *args, **kwargs):
raise MethodNotAllowed(method="GET")
@extend_schema(exclude=True)
def retrieve(self, request, *args, **kwargs):
raise MethodNotAllowed(method="GET")
def get_serializer_class(self):
if self.action == "issue_types":
return IntegrationJiraIssueTypesSerializer
return super().get_serializer_class()
def get_filter_backends(self):
if self.action == "issue_types":
return []
return super().get_filter_backends()
def get_queryset(self):
tenant_id = self.request.tenant_id
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all findings
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
@@ -6209,65 +6156,6 @@ class IntegrationJiraViewSet(BaseRLSViewSet):
return queryset
@extend_schema(
tags=["Integration"],
summary="Get available issue types for a Jira project",
description="Fetch the available issue types from Jira for a given project key and update the integration configuration.",
parameters=[
OpenApiParameter(
name="project_key",
type=str,
location=OpenApiParameter.QUERY,
required=True,
description="The Jira project key to fetch issue types for.",
),
],
)
@action(detail=False, methods=["get"], url_name="issue-types")
def issue_types(self, request, integration_pk=None):
integration = get_object_or_404(Integration, pk=integration_pk)
project_key = request.query_params.get("project_key")
if not project_key:
raise ValidationError({"project_key": "This query parameter is required."})
projects = integration.configuration.get("projects", {})
if project_key not in projects:
raise ValidationError(
{
"project_key": "The given project key is not available for this JIRA integration."
}
)
try:
jira = initialize_prowler_integration(integration)
fetched_issue_types = jira.get_available_issue_types(project_key)
except Exception as e:
logger.error(
f"Failed to fetch issue types from Jira for integration {integration_pk}, "
f"project {project_key}: {e}"
)
raise ValidationError(
{
"issue_types": "Failed to fetch issue types from Jira. Please check the integration connection."
}
)
# Update the integration configuration with the fetched issue types
issue_types_config = integration.configuration.get("issue_types", {})
if not isinstance(issue_types_config, dict):
issue_types_config = {}
issue_types_config[project_key] = fetched_issue_types
with rls_transaction(str(integration.tenant_id), using="default"):
integration.configuration["issue_types"] = issue_types_config
integration.save(using="default")
serializer = IntegrationJiraIssueTypesSerializer(
{"project_key": project_key, "issue_types": fetched_issue_types}
)
return Response(data=serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["post"], url_name="dispatches")
def dispatches(self, request, integration_pk=None):
get_object_or_404(Integration, pk=integration_pk)
@@ -6939,7 +6827,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
def get_queryset(self):
"""Get the base FindingGroupDailySummary queryset with RLS filtering."""
tenant_id = self.request.tenant_id
role = get_role(self.request.user, self.request.tenant_id)
role = get_role(self.request.user)
queryset = FindingGroupDailySummary.objects.filter(tenant_id=tenant_id)
if not role.unlimited_visibility:
@@ -6949,7 +6837,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
def _get_finding_queryset(self):
"""Get the Finding queryset for resources drill-down (with RBAC)."""
role = get_role(self.request.user, self.request.tenant_id)
role = get_role(self.request.user)
providers = get_providers(role)
tenant_id = self.request.tenant_id
@@ -7219,7 +7107,6 @@ class FindingGroupViewSet(BaseRLSViewSet):
"check_id": "check_id",
"check_title": "check_title",
"severity": "severity_order",
"delta": "delta_order",
"fail_count": "fail_count",
"pass_count": "pass_count",
"muted_count": "muted_count",
@@ -7235,7 +7122,6 @@ class FindingGroupViewSet(BaseRLSViewSet):
_RESOURCE_SORT_MAP = {
"status": "status_order",
"severity": "severity_order",
"delta": "delta_order",
"first_seen_at": "first_seen_at",
"last_seen_at": "last_seen_at",
"resource.uid": "resource_uid",
@@ -7372,22 +7258,6 @@ class FindingGroupViewSet(BaseRLSViewSet):
output_field=IntegerField(),
)
),
delta_order=Max(
Case(
When(
finding__delta="new",
finding__muted=False,
then=Value(2),
),
When(
finding__delta="changed",
finding__muted=False,
then=Value(1),
),
default=Value(0),
output_field=IntegerField(),
)
),
first_seen_at=Min("finding__first_seen_at"),
last_seen_at=Max("finding__inserted_at"),
# Max() on muted_reason / check_metadata is safe because
@@ -7420,22 +7290,6 @@ class FindingGroupViewSet(BaseRLSViewSet):
output_field=IntegerField(),
)
),
"delta_order": lambda: Max(
Case(
When(
finding__delta="new",
finding__muted=False,
then=Value(2),
),
When(
finding__delta="changed",
finding__muted=False,
then=Value(1),
),
default=Value(0),
output_field=IntegerField(),
)
),
"first_seen_at": lambda: Min("finding__first_seen_at"),
"last_seen_at": lambda: Max("finding__inserted_at"),
"resource_uid": lambda: Max("resource__uid"),
@@ -7482,14 +7336,6 @@ class FindingGroupViewSet(BaseRLSViewSet):
else:
status = "MUTED"
delta_order = row.get("delta_order", 0)
if delta_order == 2:
delta = "new"
elif delta_order == 1:
delta = "changed"
else:
delta = None
results.append(
{
"resource_id": row["resource_id"],
@@ -7505,7 +7351,6 @@ class FindingGroupViewSet(BaseRLSViewSet):
"severity": SEVERITY_ORDER_REVERSE.get(
severity_order, "informational"
),
"delta": delta,
"first_seen_at": row["first_seen_at"],
"last_seen_at": row["last_seen_at"],
"muted_reason": row.get("muted_reason"),
@@ -7570,20 +7415,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
sort_param, self._FINDING_GROUP_SORT_MAP
)
if ordering:
# delta_order is a virtual sort field: expand it to a
# lexicographic ordering by (new_count, changed_count) so groups
# with more new findings rank higher, with changed_count as the
# tie-breaker (preserves the "new > changed" priority used by
# the resources endpoint, but driven by the actual counters).
expanded_ordering = []
for field in ordering:
if field.lstrip("-") == "delta_order":
sign = "-" if field.startswith("-") else ""
expanded_ordering.append(f"{sign}new_count")
expanded_ordering.append(f"{sign}changed_count")
else:
expanded_ordering.append(field)
aggregated_queryset = aggregated_queryset.order_by(*expanded_ordering)
aggregated_queryset = aggregated_queryset.order_by(*ordering)
else:
aggregated_queryset = aggregated_queryset.order_by(
"-fail_count", "-severity_order", "check_id"
-7
View File
@@ -565,12 +565,6 @@ def providers_fixture(tenants_fixture):
alias="googleworkspace_testing",
tenant_id=tenant.id,
)
provider13 = Provider.objects.create(
provider="vercel",
uid="team_abcdef1234567890ab",
alias="vercel_testing",
tenant_id=tenant.id,
)
return (
provider1,
@@ -585,7 +579,6 @@ def providers_fixture(tenants_fixture):
provider10,
provider11,
provider12,
provider13,
)
+1 -3
View File
@@ -1824,9 +1824,7 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
filter=Q(status="FAIL", muted=False),
),
# Use prefixed names to avoid conflict with model field names
agg_first_seen_at=Min(
"first_seen_at", filter=Q(delta="new", muted=False)
),
agg_first_seen_at=Min("first_seen_at"),
agg_last_seen_at=Max("inserted_at"),
agg_failing_since=Min(
"first_seen_at", filter=Q(status="FAIL", muted=False)
-29
View File
@@ -750,35 +750,6 @@ def init_parser(self):
# More arguments for the provider.
```
##### Sensitive CLI Arguments
CLI flags that accept secrets (tokens, passwords, API keys) require special handling to protect credentials from leaking in HTML output and process listings:
1. **Use `nargs="?"` with `default=None`** so the flag works both with and without an inline value. This allows the provider to fall back to an environment variable when no value is passed.
2. **Add a `SENSITIVE_ARGUMENTS` frozenset** at the top of the `arguments.py` file listing every flag that accepts secret values:
```python
SENSITIVE_ARGUMENTS = frozenset({"--your-provider-password", "--your-provider-token"})
```
Prowler automatically discovers these frozensets and uses them to redact values in HTML output and warn users who pass secrets directly on the command line.
3. **Document the environment variable** in the `help` text so users know the recommended alternative:
```python
<provider_name>_parser.add_argument(
"--your-provider-password",
nargs="?",
default=None,
metavar="PASSWORD",
help="Password for authentication. We recommend using the YOUR_PROVIDER_PASSWORD environment variable instead.",
)
```
<Warning>
Do not add new arguments that require passing secrets as CLI values without an environment variable fallback. Prowler CLI warns users when sensitive flags receive explicit values on the command line.
</Warning>
#### Step 5: Implement Mutelist
**Explanation:**
+1 -10
View File
@@ -137,7 +137,6 @@
"group": "Tutorials",
"pages": [
"user-guide/tutorials/prowler-app-sso-entra",
"user-guide/tutorials/prowler-app-sso-google-workspace",
"user-guide/tutorials/bulk-provider-provisioning",
"user-guide/tutorials/aws-organizations-bulk-provisioning"
]
@@ -275,8 +274,7 @@
{
"group": "Image",
"pages": [
"user-guide/providers/image/getting-started-image",
"user-guide/providers/image/authentication"
"user-guide/providers/image/getting-started-image"
]
},
{
@@ -298,13 +296,6 @@
"user-guide/providers/openstack/getting-started-openstack",
"user-guide/providers/openstack/authentication"
]
},
{
"group": "Vercel",
"pages": [
"user-guide/providers/vercel/getting-started-vercel",
"user-guide/providers/vercel/authentication"
]
}
]
},
@@ -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 | 29 tools | Yes |
| Prowler Cloud/App | 27 tools | Yes |
## Tool Naming Convention
@@ -60,7 +60,6 @@ Tools for searching, viewing, and analyzing cloud resources discovered by Prowle
- **`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_resource_events`** - Get the timeline of cloud API actions performed on a resource (AWS CloudTrail). Shows who did what and when, with full request/response payloads
- **`prowler_app_get_resources_overview`** - Get aggregate statistics about cloud resources as a markdown report
### Muting Management
@@ -88,7 +87,6 @@ Tools for analyzing privilege escalation chains and security misconfigurations u
- **`prowler_app_list_attack_paths_scans`** - List Attack Paths scans with filtering by provider, provider type, and scan state (available, scheduled, executing, completed, failed, cancelled)
- **`prowler_app_list_attack_paths_queries`** - Discover available Attack Paths queries for a completed scan, including query names, descriptions, and required parameters
- **`prowler_app_run_attack_paths_query`** - Execute an Attack Paths query against a completed scan and retrieve graph results with nodes (cloud resources, findings, virtual nodes) and relationships (access paths, role assumptions, security group memberships)
- **`prowler_app_get_attack_paths_cartography_schema`** - Retrieve the Cartography graph schema (node labels, relationships, properties) for writing accurate custom openCypher queries
### Compliance Management
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

@@ -11,19 +11,16 @@ Prowler App is a web application that simplifies running Prowler. It provides:
## Components
Prowler App consists of four main components:
Prowler App consists of three main components:
- **Prowler UI**: User-friendly web interface for running Prowler and viewing results, powered by Next.js
- **Prowler API**: Backend API that executes Prowler scans and stores results, built with Django REST Framework
- **Prowler SDK**: Python SDK that integrates with Prowler CLI for advanced functionality
- **Prowler MCP Server**: Model Context Protocol server that exposes AI tools for Lighthouse, the AI-powered security assistant. Required dependency for Lighthouse.
Supporting infrastructure includes:
- **PostgreSQL**: Persistent storage of scan results
- **Celery Workers**: Asynchronous execution of Prowler scans
- **Celery Beat (API Scheduler)**: Schedules recurring scans and enqueues jobs on the broker
- **Valkey**: In-memory database serving as message broker for Celery workers
- **Neo4j**: Graph database used by the Attack Paths feature to combine cloud inventory with Prowler findings (currently populated by AWS scans)
![Prowler App Architecture](/images/products/prowler-app-architecture.png)
@@ -1,37 +0,0 @@
flowchart TB
user([User / Security Team])
cli([Prowler CLI])
subgraph APP["Prowler App"]
ui["Prowler UI<br/>(Next.js)"]
api["Prowler API<br/>(Django REST Framework)"]
worker["API Worker<br/>(Celery)"]
beat["API Scheduler<br/>(Celery Beat)"]
mcp["Prowler MCP Server<br/>(Lighthouse AI tools)"]
end
sdk["Prowler SDK<br/>(Python)"]
subgraph DATA["Data Layer"]
pg[("PostgreSQL")]
valkey[("Valkey / Redis")]
neo4j[("Neo4j")]
end
providers["Providers"]
user --> ui
user --> cli
ui -->|REST| api
api --> pg
api --> valkey
beat -->|enqueue jobs| valkey
valkey -->|dispatch| worker
worker --> pg
worker -->|Attack Paths| neo4j
worker -->|invokes| sdk
cli --> sdk
api -. AI tools .-> mcp
mcp -. context .-> api
sdk --> providers
Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

-1
View File
@@ -37,7 +37,6 @@ The supported providers right now are:
| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | Repositories | UI, API, CLI |
| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI |
| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI |
| [Vercel](/user-guide/providers/vercel/getting-started-vercel) | Official | Teams / Projects | CLI |
| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI |
| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI, API |
| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI |
@@ -141,22 +141,6 @@ The following list includes all the GitHub checks with configurable variables th
|--------------------------------------------|---------------------------------------------|---------|
| `repository_inactive_not_archived` | `inactive_not_archived_days_threshold` | Integer |
## Vercel
### Configurable Checks
The following list includes all the Vercel checks with configurable variables that can be changed in the configuration YAML file:
| Check Name | Value | Type |
|-----------------------------------------------------|------------------------------------|-----------------|
| `authentication_no_stale_tokens` | `stale_token_threshold_days` | Integer |
| `authentication_token_not_expired` | `days_to_expire_threshold` | Integer |
| `deployment_production_uses_stable_target` | `stable_branches` | List of Strings |
| `domain_ssl_certificate_valid` | `days_to_expire_threshold` | Integer |
| `project_environment_no_secrets_in_plain_type` | `secret_suffixes` | List of Strings |
| `team_member_role_least_privilege` | `max_owner_percentage` | Integer |
| `team_member_role_least_privilege` | `max_owners` | Integer |
| `team_no_stale_invitations` | `stale_invitation_threshold_days` | Integer |
## Config YAML File Structure
<Note>
@@ -640,29 +624,5 @@ github:
# github.repository_inactive_not_archived
inactive_not_archived_days_threshold: 180
# Vercel Configuration
vercel:
# vercel.deployment_production_uses_stable_target
stable_branches:
- "main"
- "master"
# vercel.authentication_token_not_expired & vercel.domain_ssl_certificate_valid
days_to_expire_threshold: 7
# vercel.authentication_no_stale_tokens
stale_token_threshold_days: 90
# vercel.team_no_stale_invitations
stale_invitation_threshold_days: 30
# vercel.team_member_role_least_privilege
max_owner_percentage: 20
max_owners: 3
# vercel.project_environment_no_secrets_in_plain_type
secret_suffixes:
- "_KEY"
- "_SECRET"
- "_TOKEN"
- "_PASSWORD"
- "_API_KEY"
- "_PRIVATE_KEY"
```
+8 -24
View File
@@ -66,38 +66,22 @@ prowler <provider> --categories internet-exposed
### Shodan
Prowler can check whether any public IPs in cloud environments are exposed in Shodan using the `-N`/`--shodan` option.
Prowler allows you check if any public IPs in your Cloud environments are exposed in Shodan with the `-N`/`--shodan <shodan_api_key>` option:
#### Using the Environment Variable (Recommended)
Set the `SHODAN_API_KEY` environment variable to avoid exposing the API key in process listings and shell history:
For example, you can check if any of your AWS Elastic Compute Cloud (EC2) instances has an elastic IP exposed in Shodan:
```console
export SHODAN_API_KEY=<shodan_api_key>
prowler aws -N/--shodan <shodan_api_key> -c ec2_elastic_ip_shodan
```
Then run Prowler with the `--shodan` flag (no value needed):
Also, you can check if any of your Azure Subscription has an public IP exposed in Shodan:
```console
prowler aws --shodan -c ec2_elastic_ip_shodan
prowler azure -N/--shodan <shodan_api_key> -c network_public_ip_shodan
```
And finally, you can check if any of your GCP projects has an public IP address exposed in Shodan:
```console
prowler azure --shodan -c network_public_ip_shodan
prowler gcp -N/--shodan <shodan_api_key> -c compute_public_address_shodan
```
```console
prowler gcp --shodan -c compute_public_address_shodan
```
#### Using the CLI Flag
Alternatively, pass the API key directly on the command line:
```console
prowler aws --shodan <shodan_api_key> -c ec2_elastic_ip_shodan
```
<Warning>
Passing secret values directly on the command line exposes them in process listings and shell history. Prowler CLI displays a warning when this pattern is detected. Use the `SHODAN_API_KEY` environment variable instead.
</Warning>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

@@ -6,19 +6,17 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.19.0" />
Prowler for Google Workspace uses a **Service Account with Domain-Wide Delegation** to authenticate to the Google Workspace Admin SDK and the Cloud Identity Policy API. This allows Prowler to read directory data and domain-level application policies on behalf of a super administrator without requiring an interactive login.
Prowler for Google Workspace uses a **Service Account with Domain-Wide Delegation** to authenticate to the Google Workspace Admin SDK. This allows Prowler to read directory data on behalf of a super administrator without requiring an interactive login.
## Required Open Authorization (OAuth) Scopes
Prowler requests the following read-only OAuth 2.0 scopes:
Prowler requests the following read-only OAuth 2.0 scopes from the Google Workspace Admin SDK:
| Scope | Description |
|-------|-------------|
| `https://www.googleapis.com/auth/admin.directory.user.readonly` | Read access to user accounts and their admin status |
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar service checks) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
<Warning>
The delegated user must be a **super administrator** in your Google Workspace organization. Using a non-admin account will result in permission errors when accessing the Admin SDK.
@@ -32,24 +30,13 @@ If no GCP project exists, create one at [https://console.cloud.google.com](https
The project is only used to host the Service Account — it does not need to have any Google Workspace data in it.
### Step 2: Enable Required APIs
### Step 2: Enable the Admin SDK API
In the [Google Cloud Console](https://console.cloud.google.com), select the target project and navigate to **APIs & Services → Library**. Search for and enable each of the following APIs:
| API | Required For |
|-----|--------------|
| **Admin SDK API** | Directory service checks (users, roles, domains) |
| **Cloud Identity API** | Calendar service checks (domain-level sharing and invitation policies) |
For each API:
1. Search for the API name in the library
2. Click the API result
3. Click **Enable**
<Note>
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar checks will return no findings if the Cloud Identity API is not enabled.
</Note>
1. Navigate to the [Google Cloud Console](https://console.cloud.google.com)
2. Select the target project
3. Navigate to **APIs & Services → Library**
4. Search for **Admin SDK API**
5. Click **Enable**
### Step 3: Create a Service Account
@@ -86,7 +73,7 @@ This JSON key grants access to your Google Workspace organization. Never commit
6. In the **OAuth scopes** field, enter the following scopes as a comma-separated list:
```
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly,https://www.googleapis.com/auth/cloud-identity.policies.readonly,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly
```
7. Click **Authorize**
@@ -127,7 +114,7 @@ The delegated user must be provided via the `GOOGLEWORKSPACE_DELEGATED_USER` env
- **Use environment variables** — Never hardcode credentials in scripts or commands
- **Use a dedicated Service Account** — Create one specifically for Prowler, separate from other integrations
- **Use read-only scopes** — Prowler only requires the read-only scopes listed above
- **Use read-only scopes** — Prowler only requires the three read-only scopes listed above
- **Restrict key access** — Set file permissions to `600` on the JSON key file
- **Rotate keys regularly** — Delete and regenerate the JSON key periodically
- **Use a least-privilege super admin** — Consider using a dedicated super admin account for Prowler's delegated user rather than a personal admin account
@@ -164,7 +151,7 @@ python3 -c "import json; json.load(open('/path/to/key.json'))" && echo "Valid JS
The Service Account cannot impersonate the delegated user. This usually means Domain-Wide Delegation has not been configured, or the OAuth scopes are incorrect. Verify:
- The Service Account Client ID is correctly entered in the Admin Console
- All required OAuth scopes are included
- All three required OAuth scopes are included
- The delegated user is a super administrator
### Permission Denied on Admin SDK Calls
@@ -172,14 +159,5 @@ The Service Account cannot impersonate the delegated user. This usually means Do
If Prowler connects but returns empty results or permission errors for specific API calls:
- Confirm Domain-Wide Delegation is fully propagated (wait a few minutes after setup)
- Verify all scopes are authorized in the Admin Console
- Verify all three scopes are authorized in the Admin Console
- Ensure the delegated user is an active super administrator
### Calendar Checks Return No Findings
If the Directory checks run successfully but the Calendar checks (e.g., `calendar_external_sharing_primary_calendar`) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
- The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2)
- The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5)
- The delegated user is a super administrator (the Policy API only returns data to super admins)
- Domain-Wide Delegation has had time to propagate after adding the new scope (a few minutes)
@@ -78,7 +78,7 @@ The Service Account JSON is the full content of the key file downloaded when cre
![Check Connection](/images/providers/googleworkspace-check-connection.png)
<Note>
If the connection test fails, verify that Domain-Wide Delegation is properly configured and that all required OAuth scopes are authorized. It may take a few minutes for delegation changes to propagate. See the [Troubleshooting](/user-guide/providers/googleworkspace/authentication#troubleshooting) section for common errors.
If the connection test fails, verify that Domain-Wide Delegation is properly configured and that all three OAuth scopes are authorized. It may take a few minutes for delegation changes to propagate. See the [Troubleshooting](/user-guide/providers/googleworkspace/authentication#troubleshooting) section for common errors.
</Note>
### Step 5: Launch the Scan
@@ -1,50 +0,0 @@
---
title: "Image Authentication in Prowler"
---
Prowler's Image provider enables container image security scanning using [Trivy](https://trivy.dev/). No authentication is required for public images. Prowler supports the following authentication methods for private registries:
* [**Basic Authentication (Environment Variables)**](https://trivy.dev/latest/docs/advanced/private-registries/docker-hub/): `REGISTRY_USERNAME` and `REGISTRY_PASSWORD`
* [**Token-Based Authentication**](https://distribution.github.io/distribution/spec/auth/token/): `REGISTRY_TOKEN`
* [**Manual Docker Login**](https://docs.docker.com/reference/cli/docker/login/): Existing credentials in Docker's credential store
Prowler uses the first available method in this priority order.
## Basic Authentication (Environment Variables)
To authenticate with a username and password, set the `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables. Prowler passes these credentials to Trivy, which handles authentication with the registry transparently:
```bash
export REGISTRY_USERNAME="myuser"
export REGISTRY_PASSWORD="mypassword"
prowler image -I myregistry.io/myapp:v1.0
```
Both variables must be set for this method to activate.
## Token-Based Authentication
To authenticate using a registry token (such as a bearer or OAuth2 token), set the `REGISTRY_TOKEN` environment variable. Prowler passes the token directly to Trivy:
```bash
export REGISTRY_TOKEN="my-registry-token"
prowler image -I myregistry.io/myapp:v1.0
```
This method is useful for registries that support token-based access without requiring a username and password.
## Manual Docker Login (Fallback)
If no environment variables are set, Prowler relies on existing credentials in Docker's credential store (`~/.docker/config.json`). To configure credentials manually before scanning:
```bash
docker login myregistry.io
prowler image -I myregistry.io/myapp:v1.0
```
<Note>
This method is available in Prowler CLI only. In Prowler Cloud, use basic authentication or token-based authentication instead.
</Note>
@@ -9,69 +9,18 @@ Prowler's Image provider enables comprehensive container image security scanning
## How It Works
* **Trivy integration:** Prowler leverages [Trivy](https://trivy.dev/) to scan container images for vulnerabilities, secrets, misconfigurations, and license issues.
* **Trivy required:** Trivy must be installed and available in the system PATH before running any scan.
* **Authentication:** No registry authentication is required for public images. For private registries, credentials can be provided via environment variables or manual `docker login`.
* Check the [Image Authentication](/user-guide/providers/image/authentication) page for more details.
* **Mutelist logic:** [Filtering](https://trivy.dev/latest/docs/configuration/filtering/) is handled by Trivy, not Prowler.
* **Output formats:** Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.).
<CardGroup cols={2}>
<Card title="Prowler Cloud" icon="cloud" href="#prowler-cloud">
Scan container images using Prowler Cloud
</Card>
<Card title="Prowler CLI" icon="terminal" href="#prowler-cli">
Scan container images using Prowler CLI
</Card>
</CardGroup>
## Prowler Cloud
<VersionBadge version="5.21.0" />
### Supported Scanners
Prowler Cloud does not support scanner selection. The vulnerability, secret, and misconfiguration scanners run automatically during each scan.
### Step 1: Access Prowler Cloud
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
2. Navigate to "Configuration" > "Cloud Providers"
![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png)
3. Click "Add Cloud Provider"
![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png)
4. Select "Container Registry"
![Select Container Registry](/user-guide/img/select-container-registry.png)
5. Enter the container registry URL (e.g., `docker.io/myorg` or `myregistry.io`) and an optional alias, then click "Next"
![Add Container Registry URL](/user-guide/img/add-registry-url.png)
### Step 2: Enter Authentication and Scan Filters
6. Optionally provide [authentication](/user-guide/providers/image/authentication) credentials for private registries, then configure the following scan filters to control which images are scanned:
* **Image filter:** A regex pattern to filter repositories by name (e.g., `^prod/.*`)
* **Tag filter:** A regex pattern to filter tags within repositories (e.g., `^(latest|v\d+\.\d+\.\d+)$`)
Then click "Next"
![Image Authentication and Filters](/user-guide/img/image-authentication-filters.png)
### Step 3: Verify Connection & Start Scan
7. Review the provider configuration and click "Launch scan" to initiate the scan
![Verify Connection & Start Scan](/user-guide/img/image-verify-connection.png)
## Prowler CLI
<VersionBadge version="5.19.0" />
<Note>
The Image provider is currently available in Prowler CLI only.
</Note>
### Install Trivy
Install Trivy using one of the following methods:
@@ -106,7 +55,7 @@ Prowler CLI supports the following scanners:
* [Misconfiguration](https://trivy.dev/docs/latest/guide/scanner/misconfiguration/)
* [License](https://trivy.dev/docs/latest/guide/scanner/license/)
By default, vulnerability, secret, and misconfiguration scanners run during a scan. To specify which scanners to use, refer to the [Specify Scanners](#specify-scanners) section below.
By default, only vulnerability and secret scanners run during a scan. To specify which scanners to use, refer to the [Specify Scanners](#specify-scanners) section below.
### Scan Container Images
@@ -163,7 +112,7 @@ Valid examples:
#### Specify Scanners
To select which scanners Trivy runs, use the `--scanners` option:
To select which scanners Trivy runs, use the `--scanners` option. By default, Prowler enables `vuln` and `secret` scanners:
```bash
# Vulnerability scanning only
@@ -323,7 +272,7 @@ To scan images from private registries, the Image provider supports three authen
#### 1. Basic Authentication (Environment Variables)
To authenticate with a username and password, set the `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables. Prowler passes these credentials to Trivy, which handles authentication with the registry transparently:
To authenticate with a username and password, set the `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables. Prowler automatically runs `docker login`, pulls the image, and performs a `docker logout` after the scan completes:
```bash
export REGISTRY_USERNAME="myuser"
@@ -332,7 +281,7 @@ export REGISTRY_PASSWORD="mypassword"
prowler image -I myregistry.io/myapp:v1.0
```
Both variables must be set for this method to activate.
Both variables must be set for this method to activate. Prowler handles the full lifecycle — login, pull, scan, and cleanup — without any manual Docker commands.
#### 2. Token-Based Authentication
@@ -357,7 +306,7 @@ prowler image -I myregistry.io/myapp:v1.0
```
<Note>
Credentials provided via environment variables are only passed to the Trivy subprocess and are not persisted beyond the scan.
When basic authentication is active (method 1), Prowler automatically logs out from all authenticated registries after the scan completes. Manual `docker login` sessions (method 3) are not affected by this cleanup.
</Note>
### Troubleshooting Common Scan Errors
@@ -1,137 +0,0 @@
---
title: "Vercel Authentication in Prowler"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.21.0" />
Prowler for Vercel authenticates using an **API Token**.
## Required Permissions
Prowler requires read-only access to Vercel teams, projects, deployments, domains, and security settings. The API Token must have access to the target team scope.
<Note>
Vercel API Tokens inherit the permissions of the user that created them. Ensure the user has at least a **Viewer** role on the team to be scanned.
</Note>
| Resource | Access | Description |
|----------|--------|-------------|
| Teams | Read | Required to list teams, members, and SSO configuration |
| Projects | Read | Required to list projects, environment variables, and deployment protection settings |
| Deployments | Read | Required to list deployments and protection status |
| Domains | Read | Required to list domains, DNS records, and SSL certificates |
| Firewall | Read | Required to read WAF rules, rate limiting, and IP blocking configuration |
---
## API Token
### Step 1: Create an API Token
1. Log into the [Vercel Dashboard](https://vercel.com/dashboard).
2. Click the account avatar in the bottom-left corner and select "Settings".
![Vercel Account Settings](/user-guide/providers/vercel/images/vercel-account-settings.png)
3. In the left sidebar, click "Tokens".
4. Under **Create Token**, enter a descriptive name (e.g., "Prowler Scan").
5. Select the **Scope** — choose the team to be scanned or "Full Account" for all teams.
6. Set an **Expiration** date, or select "No expiration" for continuous scanning.
7. Click **Create**.
![Create Vercel Token](/user-guide/providers/vercel/images/vercel-create-token.png)
8. Copy the token immediately.
<Warning>
Vercel only displays the token once. Copy it immediately and store it securely. If lost, a new token must be created.
</Warning>
### Step 2: Provide the Token to Prowler
Export the token as an environment variable:
```console
export VERCEL_TOKEN="your-api-token-here"
prowler vercel
```
---
## Team Scoping (Optional)
By default, Prowler auto-discovers all teams the authenticated user belongs to and scans each one. To restrict the scan to a specific team, provide the Team ID.
### Locate the Team ID
1. In the Vercel Dashboard, navigate to "Settings" for the target team.
2. Scroll down to the **Team ID** section and copy the value.
![Vercel Team ID](/user-guide/providers/vercel/images/vercel-team-id.png)
### Provide the Team ID to Prowler
Export the Team ID as an environment variable:
```console
export VERCEL_TOKEN="your-api-token-here"
export VERCEL_TEAM="team_yourteamid"
prowler vercel
```
---
## Environment Variables Reference
| Variable | Required | Description |
|----------|----------|-------------|
| `VERCEL_TOKEN` | Yes | Vercel API Bearer Token |
| `VERCEL_TEAM` | No | Team ID or slug to scope the scan to a single team |
---
## Best Practices
- **Create a dedicated token for Prowler** — Avoid reusing tokens shared with other integrations.
- **Use environment variables** — Never hardcode credentials in scripts or commands.
- **Scope tokens to specific teams** — When possible, limit token access to the team being scanned.
- **Set token expiration** — Use time-limited tokens and rotate them regularly.
- **Use least privilege** — Assign the Viewer role to the user creating the token unless write access is explicitly needed.
---
## Troubleshooting
### "Vercel credentials not found" Error
This error occurs when no API Token is provided. Ensure the `VERCEL_TOKEN` environment variable is set:
```console
export VERCEL_TOKEN="your-api-token-here"
```
### "Invalid or expired Vercel API token" Error
- Verify the API Token is correct and has not expired.
- Check that the token has not been revoked in the Vercel Dashboard under "Settings" > "Tokens".
### "Insufficient permissions" Error
- Ensure the user that created the token has at least a **Viewer** role on the target team.
- If scanning a specific team, verify the token scope includes that team.
### "Team not found or not accessible" Error
This error occurs when the provided `VERCEL_TEAM` value does not match an accessible team. Verify the Team ID is correct:
1. Navigate to the team "Settings" in the Vercel Dashboard.
2. Copy the exact **Team ID** value from the settings page.
### "Rate limit exceeded" Error
Vercel applies rate limits to API requests. Prowler automatically retries rate-limited requests up to 3 times with exponential backoff. If this error persists:
- Reduce the number of projects being scanned in a single run using the `--project` argument.
- Wait a few minutes and retry the scan.
@@ -1,108 +0,0 @@
---
title: "Getting Started With Vercel on Prowler"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler for Vercel scans teams and projects for security misconfigurations, including deployment protection, environment variable exposure, WAF rules, domain configuration, team access controls, and more.
## Prerequisites
Set up authentication for Vercel with the [Vercel Authentication](/user-guide/providers/vercel/authentication) guide before starting:
- Create a Vercel API Token with access to the target team
- Identify the Team ID (optional, required to scope the scan to a single team)
## Prowler CLI
<VersionBadge version="5.22.0" />
### Step 1: Set Up Authentication
Follow the [Vercel Authentication](/user-guide/providers/vercel/authentication) guide to create an API Token, then export it:
```console
export VERCEL_TOKEN="your-api-token-here"
```
Optionally, scope the scan to a specific team:
```console
export VERCEL_TEAM="team_yourteamid"
```
### Step 2: Run the First Scan
Run a baseline scan after credentials are configured:
```console
prowler vercel
```
Prowler automatically discovers all teams accessible with the provided token and runs security checks against them.
### Step 3: Filter the Scan Scope (Optional)
#### Filter by Team
To scan a specific team, set the `VERCEL_TEAM` environment variable with the Team ID or slug:
```console
export VERCEL_TEAM="team_yourteamid"
prowler vercel
```
<Note>
When no team is specified, Prowler auto-discovers all teams the authenticated user belongs to and scans each one.
</Note>
#### Filter by Project
To scan only specific projects, use the `--project` argument:
```console
prowler vercel --project my-project-name
```
Multiple projects can be specified:
```console
prowler vercel --project my-project-name another-project
```
Project IDs are also supported:
```console
prowler vercel --project prj_abc123def456
```
### Step 4: Use a Custom Configuration (Optional)
Prowler uses a configuration file to customize provider behavior. The Vercel configuration includes:
```yaml
vercel:
# Maximum number of retries for API requests (default is 3)
max_retries: 3
```
To use a custom configuration:
```console
prowler vercel --config-file /path/to/config.yaml
```
---
## Supported Services
Prowler for Vercel includes security checks across the following services:
| Service | Description |
|---------|-------------|
| **Authentication** | Token expiration and staleness checks |
| **Deployment** | Preview deployment access and production stability |
| **Domain** | DNS configuration, SSL certificates, and wildcard exposure |
| **Project** | Deployment protection, environment variable security, fork protection, and skew protection |
| **Security** | Web Application Firewall (WAF), rate limiting, IP blocking, and managed rulesets |
| **Team** | SSO enforcement, directory sync, member access, and invitation hygiene |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

@@ -79,18 +79,6 @@ Each Jira integration provides management actions through dedicated buttons:
| **Enable/Disable** | Toggle integration status | • Enable or disable integration<br/>| Status change takes effect immediately |
| **Delete** | Remove integration permanently | • Permanently delete integration<br/>• Remove all configuration data | ⚠️ **Cannot be undone** - confirm before deleting |
## Known Limitations
### Issue Types with Required Custom Fields
Certain Jira issue types (such as Epic) may require mandatory custom fields that Prowler does not currently populate when creating work items. If a selected issue type enforces required fields beyond the standard set (e.g., "Team", "Epic Name"), the work item creation will fail.
To avoid this, select an issue type that does not require additional custom fields — **Task**, **Bug**, or **Story** typically work without restrictions. If unsure which issue types are available for a project, Prowler automatically fetches and displays them in the "Issue Type" selector when sending a finding.
<Note>
Support for custom field mapping is planned for a future release.
</Note>
## Troubleshooting
### Connection test fails
@@ -1,287 +0,0 @@
---
title: 'SAML SSO: Google Workspace'
---
This page explains how to configure SAML-based Single Sign-On (SSO) in Prowler App using **Google Workspace** as the Identity Provider (IdP). The setup is divided into two parts: create a custom SAML app in Google Admin Console, then complete the configuration in Prowler App.
<Info>
**Parallel Setup Required**
Google Admin Console requires the ACS URL and Entity ID from Prowler App, while Prowler App displays these values only after opening the SAML configuration dialog. To work around this, open Prowler App in a separate browser tab, navigate to the profile page, open the "Configure SAML SSO" dialog, and copy the ACS URL and Entity ID before proceeding with the Google configuration.
</Info>
## Prerequisites
- **Google Workspace**: Super Admin access (or delegated admin with app management permissions).
- **Prowler App**: Administrator access to the organization (role with "Manage Account" permission).
- Prowler App version **5.9.0** or later.
---
## Part A - Google Admin Console
### Step 1: Navigate to Web & Mobile Apps
1. Go to [admin.google.com](https://admin.google.com).
2. In the left sidebar, navigate to **Apps > Web and mobile apps**.
3. Click "Add app", then select "Add custom SAML app".
![Google Admin Console - Web & mobile apps](/images/prowler-app/saml/saml-sso-gw-1.png)
### Step 2: Enter App Details
1. In the **App name** field, enter a name (e.g., `Prowler`).
2. Optionally, add a description (e.g., `Prowler SAML APP`) and upload a logo.
3. Click "Continue".
![Add custom SAML app - App details](/images/prowler-app/saml/saml-sso-gw-2.png)
### Step 3: Download the IdP Metadata
On the **Google Identity Provider details** screen:
1. Google displays two options:
- **Option 1**: Click "Download Metadata" to save the XML file directly. This is the recommended approach.
- **Option 2**: Manually copy the **SSO URL**, **Entity ID**, and **Certificate**.
2. Download the metadata. This file is required to complete the Prowler App configuration in Part B.
3. Click "Continue".
![Google Identity Provider details - Download metadata](/images/prowler-app/saml/saml-sso-gw-3.png)
<Warning>
**Save the Metadata File**
Download and save the IdP metadata XML file before proceeding. This file cannot be easily retrieved later and is required to complete the SAML configuration in Prowler App.
</Warning>
### Step 4: Configure the Service Provider Details
Enter the following values obtained from the SAML SSO configuration dialog in Prowler App (see [Part B, Step 1](#step-1-open-the-saml-configuration-dialog) for details on where to find them):
| Google Workspace Field | Value |
|------------------------|-------|
| **ACS URL** | The Assertion Consumer Service (ACS) URL displayed in Prowler App (e.g., `https://api.prowler.com/api/v1/accounts/saml/your-domain.com/acs/`). Self-hosted deployments use a different base URL. |
| **Entity ID** | The Audience URI displayed in Prowler App (e.g., `urn:prowler.com:sp`). |
| **Name ID format** | Select `EMAIL` from the dropdown. |
| **Name ID** | Select `Basic Information > Primary email` from the dropdown. |
Click "Continue".
![Service provider details - ACS URL, Entity ID, and Name ID configuration](/images/prowler-app/saml/saml-sso-gw-4.png)
### Step 5: Configure Attribute Mapping
To correctly provision users, configure the IdP to send the following attributes in the SAML assertion. The **App Attribute (SAML)** column lists the attribute names that Prowler expects. The **Google Directory Attribute** column shows a recommended source field, but any Google directory attribute can be used as long as it is mapped to the correct Prowler attribute name.
Click "Add mapping" for each entry:
| Google Directory Attribute | App Attribute (SAML) | Required | Notes |
|----------------------------|----------------------|----------|-------|
| `Basic Information > First name` | `firstName` | Yes | |
| `Basic Information > Last name` | `lastName` | Yes | |
| `Employee Details > Department` | `userType` | No | Determines the Prowler role. **Case-sensitive.** |
| `Employee Details > Organization` | `organization` | No | Company name displayed in Prowler App profile. |
<Info>
**Remember the Mapped Fields**
Take note of which Google directory attributes are mapped to each Prowler attribute. To update a user's role or organization in Prowler, modify the corresponding field in the user's Google Workspace profile (e.g., **Department** if mapped to `userType`). Changes propagate to Prowler on the next SAML login.
</Info>
Click "Finish" to create the SAML app.
![Attribute mapping - Google Directory attributes to Prowler SAML attributes](/images/prowler-app/saml/saml-sso-gw-5.png)
<Info>
**Dynamic Updates**
Prowler App updates user attributes each time a user logs in. Any changes made in Google Workspace are reflected on the next login.
</Info>
<Warning>
**Role Assignment via `userType`**
The `userType` attribute controls which Prowler role is assigned to the user:
- If `userType` matches an existing Prowler role name, the user receives that role automatically.
- If `userType` does not match any existing role, Prowler App creates a new role with that name **without permissions**.
- If `userType` is not set, the user receives the `no_permissions` role.
In all cases where the resulting role has no permissions, a Prowler administrator must configure the appropriate permissions through the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab. The `userType` value is **case-sensitive** - for example, `Backend` and `backend` are treated as different roles.
</Warning>
### Step 6: Enable the App for Users
By default, newly created SAML apps have user access set to **OFF**. To enable access:
1. Return to **Apps > Web and mobile apps** and select the Prowler SAML app.
2. Click "User access" (or "View details" under the "User access" section).
3. Set the service status to **ON for everyone**, or enable it for specific organizational units or groups.
4. Click "Save".
![Service Status - Set to ON for everyone](/images/prowler-app/saml/saml-sso-gw-17.png)
5. Verify in the apps list that the "User access" column displays **"ON for everyone"**.
![Web & mobile apps list - User access confirmed as "ON for everyone"](/images/prowler-app/saml/saml-sso-gw-19.png)
<Info>
**Propagation Delay**
Changes to the app status can take up to 24 hours to propagate across Google Workspace, although they typically take effect within a few minutes.
</Info>
<Info>
**"Can't Test SAML Login" Error**
If attempting to use the "Test SAML login" option in Google Admin Console and receiving a "Can't test SAML login" message, click "Allow Access" to enable the app for the organizational unit that includes the admin account. This is the same as setting the service status to **ON** as described above.
![Test SAML login - Allow access prompt](/images/prowler-app/saml/saml-sso-gw-15.png)
</Info>
---
## Part B - Prowler App Configuration
### Step 1: Open the SAML Configuration Dialog
1. Navigate to the profile settings page:
- **Prowler Cloud**: `https://cloud.prowler.com/profile`
- **Self-hosted**: `http://{your-domain}/profile`
2. Find the "SAML SSO Integration" card and click "Enable" (or "Update" if already configured).
3. The "Configure SAML SSO" dialog opens, displaying:
- **ACS URL**: The Assertion Consumer Service URL (copy this value for Part A, Step 4). This URL updates dynamically when the email domain is entered.
- **Audience**: The Entity ID (copy this value for Part A, Step 4).
- **Name ID Format**: The expected format (`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`).
- **Supported Assertion Attributes**: The list of accepted attributes (`firstName`, `lastName`, `userType`, `organization`).
![Prowler App - Configure SAML SSO dialog (initial state)](/images/prowler-app/saml/saml-sso-gw-prowler-1.png)
### Step 2: Enter the Email Domain and Upload Metadata
1. Enter the **email domain** for the organization (e.g., `prowler.cloud`). Prowler App uses this domain to identify users who should authenticate via SAML. The ACS URL updates automatically to reflect the configured domain.
2. Upload the **metadata XML file** downloaded in Part A, Step 3.
3. Click "Save".
![Prowler App - Configure SAML SSO dialog (domain entered and ready to save)](/images/prowler-app/saml/saml-sso-gw-prowler-2.png)
### Step 3: Verify the Enabled Status
The "SAML SSO Integration" card should now display a **"Status: Enabled"** indicator with a checkmark, confirming that the configuration is complete.
![Prowler App - SAML SSO Integration status showing "Enabled"](/images/prowler-app/saml/saml-sso-gw-prowler-3.png)
---
## Testing the Integration
### Optional: Create a Test User in Google Workspace
To verify the integration without affecting existing users, create a dedicated test user in Google Admin Console:
1. Navigate to **Directory > Users** in Google Admin Console.
2. Click "Add new user".
![Google Admin Console - Users directory](/images/prowler-app/saml/saml-sso-gw-7.png)
3. Fill in the user details (first name, last name, and primary email address in the configured domain).
![Add new user form](/images/prowler-app/saml/saml-sso-gw-8.png)
4. Complete the user creation. Google Workspace generates temporary credentials for the new account.
![User created successfully - Username and temporary password](/images/prowler-app/saml/saml-sso-gw-10.png)
### Optional: Configure User Attributes for Role Mapping
To test the `userType` → role mapping, set the **Department** attribute in the test user's profile. This value is sent as the `userType` SAML attribute based on the mapping configured in Part A, Step 5.
1. In **Directory > Users**, click the test user's name to open the profile.
2. Click "User details", scroll to **Employee information**, and enter a value in the **Department** field (e.g., `Backend`). This value determines the Prowler role assigned to the user.
3. Click "Save".
![User information - Setting Department to "Backend" for userType mapping](/images/prowler-app/saml/saml-sso-gw-13.png)
### SP-Initiated SSO (from Prowler)
1. Navigate to the Prowler login page.
2. Click "Continue with SAML SSO".
3. Enter an email from the configured domain (e.g., `adrian@prowler.cloud`).
4. Click "Log in". The browser redirects to Google for authentication and returns to Prowler App upon success.
![Prowler App - Sign in with SAML SSO](/images/prowler-app/saml/saml-sso-gw-prowler-4.png)
### Verify User Profile and Role Mapping
After a successful SSO login, the user profile in Prowler App reflects the attributes sent by Google Workspace:
- **Name**: Populated from the `firstName` and `lastName` attributes.
- **Role**: Created automatically from the `userType` attribute (e.g., `Backend`). If the role did not exist previously, it is created with no permissions by default.
- **Permissions**: In the screenshot below, the user has no permissions because the `Backend` role did not exist prior to login and was created automatically without any permissions. To resolve this, a Prowler administrator can either:
- Assign the appropriate permissions to the new role via the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab.
- Set the `userType` attribute in the IdP to match an existing Prowler role that already has the desired permissions. The updated role is applied on the next SAML login.
For more details on role assignment behavior and attribute mapping, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso#configure-attribute-mapping-in-the-idp) page.
![Prowler App - User profile showing role "Backend" created from userType mapping](/images/prowler-app/saml/saml-sso-gw-prowler-5.png)
### IdP-Initiated SSO (from Google)
1. Sign in to Google Workspace with an account that has access to the Prowler SAML app.
2. Open the Google Workspace app launcher (the grid icon in the top-right corner of any Google page).
3. Click the Prowler app tile.
4. The browser redirects directly to Prowler App, authenticated.
For more information on the SSO login flows, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso#idp-initiated-sso) page.
---
## Troubleshooting
<Warning>
**User Lockout After Misconfiguration**
If SAML is configured with incorrect metadata or an incorrect domain, users who authenticated via SAML cannot fall back to password login. A Prowler administrator must remove the SAML configuration via the API:
```bash
curl -X DELETE 'https://api.prowler.com/api/v1/saml-config' \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Accept: application/vnd.api+json'
```
After removal, affected users must reset their password to regain access using standard email and password login. This also applies when SAML is intentionally removed - all SAML-authenticated users need to reset their password. For more details, refer to the [SAML API Reference](/user-guide/tutorials/prowler-app-sso#saml-api-reference). For additional support, contact [Prowler Support](https://docs.prowler.com/user-guide/contact-support).
</Warning>
<Info>
**Email Domain Uniqueness**
Prowler does not allow two tenants to share the same email domain. If the domain is already associated with another tenant, the configuration will fail. This is by design to prevent authentication ambiguity.
</Info>
<Info>
**Just-in-Time Provisioning**
Users who authenticate via SAML for the first time are automatically created in Prowler App. No prior invitation is needed. User attributes (`firstName`, `lastName`, `userType`) are updated on every login from the Google directory.
</Info>
---
## Quick Summary
1. In **Google Admin Console**, create a custom SAML app using the ACS URL and Entity ID from Prowler App.
2. Configure **attribute mapping**: `firstName`, `lastName`, and optionally `userType` and `organization`.
3. **Download the metadata XML** from Google.
4. **Enable the app** in Google Workspace for the relevant users or groups.
5. In **Prowler App**, enter the email domain, upload the metadata XML, and save.
6. Verify the SAML SSO Integration shows **"Status: Enabled"**.
7. Test login via "Continue with SAML SSO" on the Prowler login page.
@@ -75,7 +75,7 @@ Choose a Method:
<Info>
**IdP Configuration**
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, Google Workspace, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application. For SSO integration with Azure AD / Entra ID, see our [Entra ID configuration instructions](/user-guide/tutorials/prowler-app-sso-entra). For Google Workspace, see our [Google Workspace configuration instructions](/user-guide/tutorials/prowler-app-sso-google-workspace).
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application. For SSO integration with Azure AD / Entra ID, see our [Entra ID configuration instructions](/user-guide/tutorials/prowler-app-sso-entra).
</Info>
@@ -88,7 +88,7 @@ Choose a Method:
| `firstName` | The user's first name. | Yes |
| `lastName` | The user's last name. | Yes |
| `userType` | Determines which Prowler role the user receives (e.g., `admin`, `auditor`). If a role with that name already exists, the user receives it automatically; if it does not exist, Prowler App creates a new role with that name without permissions. If `userType` is not defined, the user is assigned the `no_permissions` role. Role permissions can be edited in the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). | No |
| `organization` | The user's company name. | No |
| `companyName` | The user's company name. This is automatically populated if the IdP sends an `organization` attribute. | No |
<Info>
**IdP Attribute Mapping**
-12
View File
@@ -2,18 +2,6 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.6.0] (Prowler UNRELEASED)
### 🚀 Added
- Resource events tool to get timeline for a resource (who, what, when) [(#10412)](https://github.com/prowler-cloud/prowler/pull/10412)
### 🔐 Security
- `authlib` bumped from 1.6.5 to 1.6.9 to fix CVE-2026-28802 (JWT `alg: none` validation bypass) [(#10579)](https://github.com/prowler-cloud/prowler/pull/10579)
---
## [0.5.0] (Prowler v5.21.0)
### 🚀 Added
@@ -135,48 +135,3 @@ class ResourcesMetadataResponse(BaseModel):
regions=attributes.get("regions"),
types=attributes.get("types"),
)
class ResourceEvent(MinimalSerializerMixin, BaseModel):
"""A cloud API action performed on a resource.
Sourced from cloud provider audit logs (AWS CloudTrail, Azure Activity Logs,
GCP Audit Logs, etc.).
"""
id: str
event_time: str
event_name: str
event_source: str
actor: str
actor_uid: str | None = None
actor_type: str | None = None
source_ip_address: str | None = None
user_agent: str | None = None
request_data: dict | None = None
response_data: dict | None = None
error_code: str | None = None
error_message: str | None = None
@classmethod
def from_api_response(cls, data: dict) -> "ResourceEvent":
"""Transform JSON:API resource event response."""
return cls(id=data["id"], **data.get("attributes", {}))
class ResourceEventsResponse(BaseModel):
"""Response wrapper for resource events list."""
events: list[ResourceEvent]
total_events: int
@classmethod
def from_api_response(cls, response: dict) -> "ResourceEventsResponse":
"""Transform JSON:API response to events list."""
data = response.get("data", [])
events = [ResourceEvent.from_api_response(item) for item in data]
return cls(
events=events,
total_events=len(events),
)
@@ -8,7 +8,6 @@ from typing import Any
from prowler_mcp_server.prowler_app.models.resources import (
DetailedResource,
ResourceEventsResponse,
ResourcesListResponse,
ResourcesMetadataResponse,
)
@@ -343,62 +342,3 @@ class ResourcesTools(BaseTool):
report = "\n".join(report_lines)
return {"report": report}
async def get_resource_events(
self,
resource_id: str = Field(
description="Prowler's internal UUID (v4) for the resource. Use `prowler_app_list_resources` to find the right ID, or get it from a finding's resource relationship via `prowler_app_get_finding_details`."
),
lookback_days: int = Field(
default=90,
ge=1,
le=90,
description="How many days back to search for events. Range: 1-90. Default: 90.",
),
page_size: int = Field(
default=50,
ge=1,
le=50,
description="Number of events to return. Range: 1-50. Default: 50.",
),
include_read_events: bool = Field(
default=False,
description="Include read-only API calls (e.g., Describe*, Get*, List*). Default: false (write/modify events only).",
),
) -> dict[str, Any]:
"""Get the timeline of cloud API actions performed on a specific resource.
IMPORTANT: Currently only available for AWS resources. Uses CloudTrail to retrieve
the modification history of a resource, showing who did what and when.
Each event includes:
- What happened: event_name (e.g., PutBucketPolicy), event_source (e.g., s3.amazonaws.com)
- Who did it: actor, actor_type, actor_uid
- From where: source_ip_address, user_agent
- What changed: request_data, response_data (full API payloads)
- Errors: error_code, error_message (if the action failed)
Use cases:
- Investigating security incidents (who modified this resource?)
- Change tracking and audit trails
- Understanding resource configuration drift
- Identifying unauthorized or unexpected modifications
Workflows:
1. Resource browsing: prowler_app_list_resources find resource this tool for event history
2. Incident investigation: prowler_app_get_finding_details get resource ID from finding this tool to identify who caused the issue, what they changed, and when
"""
params = {
"lookback_days": lookback_days,
"page[size]": page_size,
"include_read_events": include_read_events,
}
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
f"/resources/{resource_id}/events", params=clean_params
)
events_response = ResourceEventsResponse.from_api_response(api_response)
return events_response.model_dump()
+3 -3
View File
@@ -36,14 +36,14 @@ wheels = [
[[package]]
name = "authlib"
version = "1.6.9"
version = "1.6.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" },
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
]
[[package]]
Generated
+69 -83
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -605,21 +605,21 @@ requests = ">=2.21.0,<3.0.0"
[[package]]
name = "alibabacloud-tea-openapi"
version = "0.4.4"
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.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f"},
{file = "alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce"},
{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 = {version = ">=3.0.0,<47.0.0", markers = "python_version >= \"3.8\""}
cryptography = ">=3.0.0,<45.0.0"
darabonba-core = ">=1.0.3,<2.0.0"
[[package]]
@@ -836,14 +836,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a
[[package]]
name = "authlib"
version = "1.6.9"
version = "1.6.5"
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"},
{file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"},
{file = "authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a"},
{file = "authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b"},
]
[package.dependencies]
@@ -1888,6 +1888,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "contextlib2"
@@ -1982,75 +1983,62 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "cryptography"
version = "46.0.6"
version = "44.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
groups = ["main", "dev"]
files = [
{file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"},
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"},
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"},
{file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"},
{file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"},
{file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"},
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"},
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"},
{file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"},
{file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"},
{file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"},
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"},
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"},
{file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"},
{file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"},
{file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"},
{file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
{file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
{file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
{file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
{file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
{file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
{file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
]
[package.dependencies]
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
nox = ["nox[uv] (>=2024.4.15)"]
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -3083,7 +3071,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.03.6"
jsonschema-specifications = ">=2023.3.6"
referencing = ">=0.28.4"
rpds-py = ">=0.7.1"
@@ -3163,7 +3151,7 @@ files = [
]
[package.dependencies]
certifi = ">=14.05.14"
certifi = ">=14.5.14"
durationpy = ">=0.7"
google-auth = ">=1.0.1"
oauthlib = ">=3.2.2"
@@ -4086,24 +4074,23 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "oci"
version = "2.169.0"
version = "2.160.3"
description = "Oracle Cloud Infrastructure Python SDK"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b"},
{file = "oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa"},
{file = "oci-2.160.3-py3-none-any.whl", hash = "sha256:858bff3e697098bdda44833d2476bfb4632126f0182178e7dbde4dbd156d71f0"},
{file = "oci-2.160.3.tar.gz", hash = "sha256:57514889be3b713a8385d86e3ba8a33cf46e3563c2a7e29a93027fb30b8a2537"},
]
[package.dependencies]
certifi = "*"
circuitbreaker = {version = ">=1.3.1,<3.0.0", markers = "python_version >= \"3.7\""}
cryptography = ">=3.2.1,<47.0.0"
pyOpenSSL = ">=17.5.0,<27.0.0"
cryptography = ">=3.2.1,<46.0.0"
pyOpenSSL = ">=17.5.0,<25.0.0"
python-dateutil = ">=2.5.3,<3.0.0"
pytz = ">=2016.10"
urllib3 = {version = ">=2.6.3", markers = "python_version >= \"3.10.0\""}
[package.extras]
adk = ["docstring-parser (>=0.16) ; python_version >= \"3.10\" and python_version < \"4\"", "mcp (>=1.6.0) ; python_version >= \"3.10\" and python_version < \"4\"", "pydantic (>=2.10.6) ; python_version >= \"3.10\" and python_version < \"4\"", "rich (>=13.9.4) ; python_version >= \"3.10\" and python_version < \"4\""]
@@ -4976,7 +4963,7 @@ files = [
]
[package.dependencies]
astroid = ">=3.3.8,<=3.4.0-dev0"
astroid = ">=3.3.8,<=3.4.0.dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -5037,19 +5024,18 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
[[package]]
name = "pyopenssl"
version = "26.0.0"
version = "24.3.0"
description = "Python wrapper module around the OpenSSL library"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"},
{file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"},
{file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"},
{file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"},
]
[package.dependencies]
cryptography = ">=46.0.0,<47"
typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""}
cryptography = ">=41.0.5,<45"
[package.extras]
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"]
@@ -5822,10 +5808,10 @@ files = [
]
[package.dependencies]
botocore = ">=1.37.4,<2.0a.0"
botocore = ">=1.37.4,<2.0a0"
[package.extras]
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
[[package]]
name = "safety"
@@ -6743,4 +6729,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
content-hash = "91739ee5e383337160f9f08b76944ab4e8629c94084c8a9d115246862557f7c5"
content-hash = "65f1f9833d61f90f1f89ed70b3677f76c0693bae275dd39699df01c05050bbe6"
-24
View File
@@ -11,42 +11,20 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `glue_etl_jobs_no_secrets_in_arguments` check for plaintext secrets in AWS Glue ETL job arguments [(#10368)](https://github.com/prowler-cloud/prowler/pull/10368)
- `awslambda_function_no_dead_letter_queue`, `awslambda_function_using_cross_account_layers`, and `awslambda_function_env_vars_not_encrypted_with_cmk` checks for AWS Lambda [(#10381)](https://github.com/prowler-cloud/prowler/pull/10381)
- `entra_conditional_access_policy_mdm_compliant_device_required` check for M365 provider [(#10220)](https://github.com/prowler-cloud/prowler/pull/10220)
- `directory_super_admin_only_admin_roles` check for Google Workspace provider [(#10488)](https://github.com/prowler-cloud/prowler/pull/10488)
- `ec2_securitygroup_allow_ingress_from_internet_to_any_port_from_ip` check for AWS provider using `ipaddress.is_global` for accurate public IP detection [(#10335)](https://github.com/prowler-cloud/prowler/pull/10335)
- `entra_conditional_access_policy_block_o365_elevated_insider_risk` check for M365 provider [(#10232)](https://github.com/prowler-cloud/prowler/pull/10232)
- `--resource-group` and `--list-resource-groups` CLI flags to filter checks by resource group across all providers [(#10479)](https://github.com/prowler-cloud/prowler/pull/10479)
- CISA SCuBA Google Workspace Baselines compliance [(#10466)](https://github.com/prowler-cloud/prowler/pull/10466)
- CIS Google Workspace Foundations Benchmark v1.3.0 compliance [(#10462)](https://github.com/prowler-cloud/prowler/pull/10462)
- `calendar_external_sharing_primary_calendar`, `calendar_external_sharing_secondary_calendar`, and `calendar_external_invitations_warning` checks for Google Workspace provider using the Cloud Identity Policy API [(#10597)](https://github.com/prowler-cloud/prowler/pull/10597)
- `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222)
- `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234)
- `entra_conditional_access_policy_unknown_device_blocked` check for M365 provider [(#10235)](https://github.com/prowler-cloud/prowler/pull/10235)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)
### 🔄 Changed
- Added `internet-exposed` category to 13 AWS checks (CloudFront, CodeArtifact, EC2, EFS, RDS, SageMaker, Shield, VPC) [(#10502)](https://github.com/prowler-cloud/prowler/pull/10502)
- Minimum Python version from 3.9 to 3.10 and updated classifiers to reflect supported versions (3.10, 3.11, 3.12) [(#10464)](https://github.com/prowler-cloud/prowler/pull/10464)
- Platform normalization in Conditional Access checks moved to `PlatformConditions` model validator [(#10614)](https://github.com/prowler-cloud/prowler/pull/10614)
- Sensitive CLI flags now warn when values are passed directly, recommending environment variables instead [(#10532)](https://github.com/prowler-cloud/prowler/pull/10532)
### 🐞 Fixed
- OCI mutelist support: pass `tenancy_id` to `is_finding_muted` and update `oraclecloud_mutelist_example.yaml` to use `Accounts` key [(#10565)](https://github.com/prowler-cloud/prowler/issues/10565)
- `return` statements in `finally` blocks replaced across IAM, Organizations, GCP provider, and custom checks metadata to stop silently swallowing exceptions [(#10102)](https://github.com/prowler-cloud/prowler/pull/10102)
- `JiraConnection` now includes issue types per project fetched during `test_connection`, fixing `JiraInvalidIssueTypeError` on non-English Jira instances [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534)
- `--list-checks` and `--list-checks-json` now include `threat-detection` category checks in their output [(#10578)](https://github.com/prowler-cloud/prowler/pull/10578)
- Missing `__init__.py` in `codebuild_project_uses_allowed_github_organizations` check preventing discovery by `--list-checks` [(#10584)](https://github.com/prowler-cloud/prowler/pull/10584)
- Azure Key Vault checks emitting incorrect findings for keys, secrets, and vault logging [(#10332)](https://github.com/prowler-cloud/prowler/pull/10332)
- `is_policy_public` now recognizes `kms:CallerAccount`, `kms:ViaService`, `aws:CalledVia`, `aws:CalledViaFirst`, and `aws:CalledViaLast` as restrictive condition keys, fixing false positives in `kms_key_policy_is_not_public` and other checks that use `is_condition_block_restrictive` [(#10600)](https://github.com/prowler-cloud/prowler/pull/10600)
- `_enabled_regions` empty-set bug in `AwsProvider.generate_regional_clients` creating boto3 clients for all 36 AWS regions instead of the audited ones, causing random CI timeouts and slow test runs [(#10598)](https://github.com/prowler-cloud/prowler/pull/10598)
- Retrieve only the latest version from a package in AWS CodeArtifact [(#10243)](https://github.com/prowler-cloud/prowler/pull/10243)
### 🔐 Security
- Sensitive CLI flag values (tokens, keys, passwords) in HTML output "Parameters used" field now redacted to prevent credential leaks [(#10518)](https://github.com/prowler-cloud/prowler/pull/10518)
- `authlib` bumped from 1.6.5 to 1.6.9 to fix CVE-2026-28802 (JWT `alg: none` validation bypass) [(#10579)](https://github.com/prowler-cloud/prowler/pull/10579)
- `cryptography` bumped from 44.0.3 to 46.0.6 ([CVE-2026-26007](https://github.com/pyca/cryptography/security/advisories/GHSA-r6ph-v2qm-q3c2), [CVE-2026-34073](https://github.com/pyca/cryptography/security/advisories/GHSA-m959-cc7f-wv43)), `oci` to 2.169.0, and `alibabacloud-tea-openapi` to 0.4.4 [(#10535)](https://github.com/prowler-cloud/prowler/pull/10535)
---
@@ -60,8 +38,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Oracle Cloud `kms_key_rotation_enabled` now checks current key version age to avoid false positives on vaults without auto-rotation support [(#10450)](https://github.com/prowler-cloud/prowler/pull/10450)
- Oracle Cloud patch for filestorage, blockstorage, kms, and compute services in OCI to allow for region scanning outside home [(#10455)](https://github.com/prowler-cloud/prowler/pull/10472)
- Oracle cloud provider now supports multi-region filtering [(#10435)](https://github.com/prowler-cloud/prowler/pull/10473)
- `prowler image --registry` failing with `ImageNoImagesProvidedError` due to registry arguments not being forwarded to `ImageProvider` in `init_global_provider` [(#10457)](https://github.com/prowler-cloud/prowler/issues/10457)
- Oracle Cloud multi-region support for identity client configuration in blockstorage, identity, and filestorage services [(#10519)](https://github.com/prowler-cloud/prowler/pull/10520)
---
-23
View File
@@ -69,9 +69,6 @@ from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS
from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS
from prowler.lib.outputs.compliance.cis.cis_googleworkspace import GoogleWorkspaceCIS
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
from prowler.lib.outputs.compliance.cisa_scuba.cisa_scuba_googleworkspace import (
GoogleWorkspaceCISASCuBA,
)
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
from prowler.lib.outputs.compliance.compliance import display_compliance_table
@@ -145,7 +142,6 @@ from prowler.providers.mongodbatlas.models import MongoDBAtlasOutputOptions
from prowler.providers.nhn.models import NHNOutputOptions
from prowler.providers.openstack.models import OpenStackOutputOptions
from prowler.providers.oraclecloud.models import OCIOutputOptions
from prowler.providers.vercel.models import VercelOutputOptions
def prowler():
@@ -271,8 +267,6 @@ def prowler():
categories=categories,
resource_groups=resource_groups,
provider=provider,
list_checks=getattr(args, "list_checks", False)
or getattr(args, "list_checks_json", False),
)
# if --list-checks-json, dump a json file and exit
@@ -401,10 +395,6 @@ def prowler():
output_options = OpenStackOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "vercel":
output_options = VercelOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
# Run the quick inventory for the provider if available
if hasattr(args, "quick_inventory") and args.quick_inventory:
@@ -1164,19 +1154,6 @@ def prowler():
)
generated_outputs["compliance"].append(cis)
cis.batch_write_data_to_file()
elif compliance_name.startswith("cisa_scuba_"):
# Generate CISA SCuBA Finding Object
filename = (
f"{output_options.output_directory}/compliance/"
f"{output_options.output_filename}_{compliance_name}.csv"
)
cisa_scuba = GoogleWorkspaceCISASCuBA(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
file_path=filename,
)
generated_outputs["compliance"].append(cisa_scuba)
cisa_scuba.batch_write_data_to_file()
else:
filename = (
f"{output_options.output_directory}/compliance/"
@@ -54,9 +54,7 @@
{
"Id": "1.1.3",
"Description": "Ensure super admin accounts are used only for super admin activities",
"Checks": [
"directory_super_admin_only_admin_roles"
],
"Checks": [],
"Attributes": [
{
"Section": "1 Directory",
@@ -98,9 +96,7 @@
{
"Id": "3.1.1.1.1",
"Description": "Ensure external sharing options for primary calendars are configured",
"Checks": [
"calendar_external_sharing_primary_calendar"
],
"Checks": [],
"Attributes": [
{
"Section": "3 Apps",
@@ -142,9 +138,7 @@
{
"Id": "3.1.1.1.3",
"Description": "Ensure external invitation warnings for Google Calendar are configured",
"Checks": [
"calendar_external_invitations_warning"
],
"Checks": [],
"Attributes": [
{
"Section": "3 Apps",
@@ -165,9 +159,7 @@
{
"Id": "3.1.1.2.1",
"Description": "Ensure external sharing options for secondary calendars are configured",
"Checks": [
"calendar_external_sharing_secondary_calendar"
],
"Checks": [],
"Attributes": [
{
"Section": "3 Apps",
File diff suppressed because it is too large Load Diff
@@ -121,7 +121,6 @@
"defenderxdr_endpoint_privileged_user_exposed_credentials",
"defender_identity_health_issues_no_open",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_block_elevated_insider_risk",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
@@ -247,7 +246,6 @@
"entra_break_glass_account_fido2_security_key_registered",
"entra_default_app_management_policy_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_conditional_access_policy_device_code_flow_blocked",
@@ -627,7 +625,6 @@
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_managed_device_required_for_authentication",
@@ -686,8 +683,6 @@
"entra_admin_portals_access_restriction",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_block_elevated_insider_risk",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_policy_guest_users_access_restrictions",
"sharepoint_external_sharing_restricted"
@@ -709,7 +704,6 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_break_glass_account_fido2_security_key_registered",
@@ -781,7 +775,6 @@
"defender_safelinks_policy_enabled",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_block_elevated_insider_risk",
"entra_conditional_access_policy_block_o365_elevated_insider_risk"
]
},
@@ -101,7 +101,6 @@
"Id": "1.1.6",
"Description": "Ensure a managed device is required for authentication",
"Checks": [
"entra_conditional_access_policy_unknown_device_blocked",
"entra_managed_device_required_for_authentication"
],
"Attributes": [
@@ -120,7 +119,6 @@
"Id": "1.1.7",
"Description": "Ensure a managed device is required for MFA registration",
"Checks": [
"entra_conditional_access_policy_unknown_device_blocked",
"entra_managed_device_required_for_mfa_registration"
],
"Attributes": [
-1
View File
@@ -65,7 +65,6 @@ class Provider(str, Enum):
ALIBABACLOUD = "alibabacloud"
OPENSTACK = "openstack"
IMAGE = "image"
VERCEL = "vercel"
# Providers that delegate scanning to an external tool (e.g. Trivy, promptfoo)
-31
View File
@@ -609,34 +609,3 @@ cloudflare:
# Maximum number of retries for API requests (default is 2)
# Set to 0 to disable retries
max_retries: 3
# Vercel Configuration
vercel:
# vercel.deployment_production_uses_stable_target
# Branches considered stable for production deployments
stable_branches:
- "main"
- "master"
# vercel.authentication_token_not_expired & vercel.domain_ssl_certificate_valid
# Number of days before expiration to flag a token/certificate as about to expire
days_to_expire_threshold: 7
# vercel.authentication_no_stale_tokens
# Number of days of inactivity before a token is considered stale
stale_token_threshold_days: 90
# vercel.team_no_stale_invitations
# Number of days before a pending invitation is considered stale
stale_invitation_threshold_days: 30
# vercel.team_member_role_least_privilege
# Maximum percentage of team members that can have the OWNER role
max_owner_percentage: 20
# Maximum number of owners allowed (overrides percentage for large teams)
max_owners: 3
# vercel.project_environment_no_secrets_in_plain_type
# Suffixes that identify secret-like environment variable names
secret_suffixes:
- "_KEY"
- "_SECRET"
- "_TOKEN"
- "_PASSWORD"
- "_API_KEY"
- "_PRIVATE_KEY"
@@ -1,12 +1,12 @@
### Account, Check and/or Region can be * to apply for all the cases.
### Account == OCI Tenancy OCID and Region == OCI Region
### Tenancy, Check and/or Region can be * to apply for all the cases.
### Tenancy == OCI Tenancy OCID and Region == OCI Region
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Accounts, Regions, Resources and/or Tags.
### For each check you can except Tenancies, Regions, Resources and/or Tags.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
Tenancies:
"ocid1.tenancy.oc1..aaaaaaaexample":
Checks:
"iam_user_mfa_enabled":
@@ -1,50 +0,0 @@
### Account, Check and/or Region can be * to apply for all the cases.
### Account == <Vercel Team ID>
### Region == * (Vercel is a global service, region is always "global")
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Accounts, Regions, Resources and/or Tags.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
"team_example123":
Checks:
"project_deployment_protection_enabled":
Regions:
- "*"
Resources:
- "prj_internal001"
- "prj_internal002"
Description: "Mute deployment protection check for internal-only projects"
"project_environment_*":
Regions:
- "*"
Resources:
- "prj_staging.*"
Description: "Mute all environment variable checks for staging projects"
"*":
Regions:
- "*"
Resources:
- "prj_sandbox"
Tags:
- "environment=sandbox"
Description: "Mute all checks for sandbox project with matching tag"
"*":
Checks:
"security_waf_enabled":
Regions:
- "*"
Resources:
- "prj_static.*"
Description: "Mute WAF check for static-only projects across all teams"
"*":
Regions:
- "*"
Resources:
- "*"
Tags:
- "prowler-ignore=true"
Description: "Global mute for resources tagged with prowler-ignore=true"
-9
View File
@@ -713,15 +713,6 @@ def execute(
is_finding_muted_args["project_id"] = (
global_provider.identity.project_id
)
elif global_provider.type == "vercel":
team = getattr(global_provider.identity, "team", None)
is_finding_muted_args["team_id"] = (
team.id if team else global_provider.identity.user_id
)
elif global_provider.type == "oraclecloud":
is_finding_muted_args["tenancy_id"] = (
global_provider.identity.tenancy_id
)
for finding in check_findings:
if global_provider.type == "cloudflare":
is_finding_muted_args["account_id"] = finding.account_id
+1 -7
View File
@@ -20,7 +20,6 @@ def load_checks_to_execute(
compliance_frameworks: list = None,
categories: set = None,
resource_groups: set = None,
list_checks: bool = False,
) -> set:
"""Generate the list of checks to execute based on the cloud provider and the input arguments given"""
try:
@@ -210,12 +209,7 @@ def load_checks_to_execute(
):
checks_to_execute.add(check_name)
# Only execute threat detection checks if threat-detection category is set
# Skip this exclusion when listing checks (--list-checks or --list-checks-json)
if (
(not categories or "threat-detection" not in categories)
and not check_list
and not list_checks
):
if (not categories or "threat-detection" not in categories) and not check_list:
for threat_detection_check in check_categories.get("threat-detection", []):
checks_to_execute.discard(threat_detection_check)
-44
View File
@@ -1240,50 +1240,6 @@ class CheckReportMongoDBAtlas(Check_Report):
self.location = getattr(resource, "location", self.project_id)
@dataclass
class CheckReportVercel(Check_Report):
"""Contains the Vercel Check's finding information.
Vercel is a global platform - team_id is the scoping context.
All resource-related attributes are derived from the resource object.
"""
resource_name: str
resource_id: str
team_id: str
def __init__(
self,
metadata: Dict,
resource: Any,
resource_name: str = None,
resource_id: str = None,
team_id: str = None,
) -> None:
"""Initialize the Vercel Check's finding information.
Args:
metadata: Check metadata dictionary
resource: The Vercel resource being checked
resource_name: Override for resource name
resource_id: Override for resource ID
team_id: Override for team ID
"""
super().__init__(metadata, resource)
self.resource_name = resource_name or getattr(
resource, "name", getattr(resource, "resource_name", "")
)
self.resource_id = resource_id or getattr(
resource, "id", getattr(resource, "resource_id", "")
)
self.team_id = team_id or getattr(resource, "team_id", "")
@property
def region(self) -> str:
"""Vercel is global - return 'global'."""
return "global"
# Testing Pending
def load_check_metadata(metadata_file: str) -> CheckMetadata:
"""
+3 -9
View File
@@ -12,7 +12,6 @@ from prowler.config.config import (
default_output_directory,
)
from prowler.lib.check.models import Severity
from prowler.lib.cli.redact import warn_sensitive_argument_values
from prowler.lib.outputs.common import Status
from prowler.providers.common.arguments import (
init_providers_parser,
@@ -28,10 +27,10 @@ class ProwlerArgumentParser:
self.parser = argparse.ArgumentParser(
prog="prowler",
formatter_class=RawTextHelpFormatter,
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel,dashboard,iac,image} ...",
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,dashboard,iac,image} ...",
epilog="""
Available Cloud Providers:
{aws,azure,gcp,kubernetes,m365,github,googleworkspace,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,vercel}
{aws,azure,gcp,kubernetes,m365,github,googleworkspace,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack}
aws AWS Provider
azure Azure Provider
gcp GCP Provider
@@ -48,7 +47,6 @@ Available Cloud Providers:
image Container Image Provider
nhn NHN Provider (Unofficial)
mongodbatlas MongoDB Atlas Provider (Beta)
vercel Vercel Provider
Available components:
dashboard Local dashboard
@@ -125,10 +123,6 @@ Detailed documentation at https://docs.prowler.com
elif sys.argv[1] == "oci":
sys.argv[1] = "oraclecloud"
# Warn about sensitive flags passed with explicit values
# Snapshot argv before parse_args() which may exit on errors
warn_sensitive_argument_values(list(sys.argv[1:]))
# Parse arguments
args = self.parser.parse_args()
@@ -437,7 +431,7 @@ Detailed documentation at https://docs.prowler.com
nargs="?",
default=None,
metavar="SHODAN_API_KEY",
help="Check if any public IPs in your Cloud environments are exposed in Shodan. We recommend to use the SHODAN_API_KEY environment variable to provide the API key.",
help="Check if any public IPs in your Cloud environments are exposed in Shodan.",
)
third_party_subparser.add_argument(
"--slack",
-113
View File
@@ -1,113 +0,0 @@
from functools import lru_cache
from importlib import import_module
from colorama import Fore, Style
from prowler.lib.cli.sensitive import SENSITIVE_ARGUMENTS as COMMON_SENSITIVE_ARGUMENTS
from prowler.lib.logger import logger
from prowler.providers.common.provider import Provider, providers_path
REDACTED_VALUE = "REDACTED"
@lru_cache(maxsize=None)
def get_sensitive_arguments() -> frozenset:
"""Collect SENSITIVE_ARGUMENTS from all provider argument modules and the common parser."""
sensitive: set[str] = set()
# Common parser sensitive arguments (e.g., --shodan)
sensitive.update(COMMON_SENSITIVE_ARGUMENTS)
# Provider-specific sensitive arguments
for provider in Provider.get_available_providers():
try:
module = import_module(
f"{providers_path}.{provider}.lib.arguments.arguments"
)
sensitive.update(getattr(module, "SENSITIVE_ARGUMENTS", frozenset()))
except Exception as error:
logger.debug(f"Could not load SENSITIVE_ARGUMENTS from {provider}: {error}")
return frozenset(sensitive)
def redact_argv(argv: list[str]) -> str:
"""Redact values of sensitive CLI flags from an argument list.
Handles both ``--flag value`` and ``--flag=value`` syntax.
Returns a single joined string suitable for display.
"""
sensitive = get_sensitive_arguments()
result: list[str] = []
skip_next = False
for i, arg in enumerate(argv):
if skip_next:
result.append(REDACTED_VALUE)
skip_next = False
continue
# Handle --flag=value syntax
if "=" in arg:
flag = arg.split("=", 1)[0]
if flag in sensitive:
result.append(f"{flag}={REDACTED_VALUE}")
continue
# Handle --flag value syntax
if arg in sensitive:
result.append(arg)
# Only redact the next token if it exists and is not another flag
if i + 1 < len(argv) and not argv[i + 1].startswith("-"):
skip_next = True
continue
result.append(arg)
return " ".join(result)
def warn_sensitive_argument_values(argv: list[str]) -> None:
"""Log a warning for each sensitive CLI flag that was passed with an explicit value.
Scans the raw argv list (not parsed args) to detect when users pass
secret values directly on the command line instead of using environment
variables. Handles both ``--flag value`` and ``--flag=value`` syntax.
Args:
argv: The argument list to check (typically ``sys.argv[1:]``).
"""
sensitive = get_sensitive_arguments()
if not sensitive:
return
use_color = "--no-color" not in argv
flags_with_values: list[str] = []
for i, arg in enumerate(argv):
# --flag=value syntax
if "=" in arg:
flag = arg.split("=", 1)[0]
if flag in sensitive:
flags_with_values.append(flag)
continue
# --flag value syntax
if arg in sensitive:
if i + 1 < len(argv) and not argv[i + 1].startswith("-"):
flags_with_values.append(arg)
for flag in flags_with_values:
if use_color:
logger.warning(
f"{Fore.YELLOW}{Style.BRIGHT}WARNING:{Style.RESET_ALL}{Fore.YELLOW} "
f"Passing a value directly to {flag} is not recommended. "
f"Use the corresponding environment variable instead to avoid "
f"exposing secrets in process listings and shell history.{Style.RESET_ALL}"
)
else:
logger.warning(
f"Passing a value directly to {flag} is not recommended. "
f"Use the corresponding environment variable instead to avoid "
f"exposing secrets in process listings and shell history."
)
-8
View File
@@ -1,8 +0,0 @@
"""Common parser sensitive arguments.
This module is kept dependency-free (no prowler-internal imports) so that
``prowler.lib.cli.redact`` and any provider argument module can import it
without circular-import risk.
"""
SENSITIVE_ARGUMENTS = frozenset({"--shodan"})
@@ -1,90 +0,0 @@
from prowler.config.config import timestamp
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.compliance.cisa_scuba.models import (
GoogleWorkspaceCISASCuBAModel,
)
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
from prowler.lib.outputs.finding import Finding
class GoogleWorkspaceCISASCuBA(ComplianceOutput):
"""
This class represents the Google Workspace CISA SCuBA compliance output.
Attributes:
- _data (list): A list to store transformed data from findings.
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
Methods:
- transform: Transforms findings into Google Workspace CISA SCuBA compliance format.
"""
def transform(
self,
findings: list[Finding],
compliance: Compliance,
compliance_name: str,
) -> None:
"""
Transforms a list of findings into Google Workspace CISA SCuBA compliance format.
Parameters:
- findings (list): A list of findings.
- compliance (Compliance): A compliance model.
- compliance_name (str): The name of the compliance model.
Returns:
- None
"""
for finding in findings:
# Get the compliance requirements for the finding
finding_requirements = finding.compliance.get(compliance_name, [])
for requirement in compliance.Requirements:
if requirement.Id in finding_requirements:
for attribute in requirement.Attributes:
compliance_row = GoogleWorkspaceCISASCuBAModel(
Provider=finding.provider,
Description=compliance.Description,
Domain=finding.account_name,
AssessmentDate=str(timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Attributes_Section=attribute.Section,
Requirements_Attributes_SubSection=attribute.SubSection,
Requirements_Attributes_Service=attribute.Service,
Requirements_Attributes_Type=attribute.Type,
Status=finding.status,
StatusExtended=finding.status_extended,
ResourceId=finding.resource_uid,
ResourceName=finding.resource_name,
CheckId=finding.check_id,
Muted=finding.muted,
Framework=compliance.Framework,
Name=compliance.Name,
)
self._data.append(compliance_row)
# Add manual requirements to the compliance output
for requirement in compliance.Requirements:
if not requirement.Checks:
for attribute in requirement.Attributes:
compliance_row = GoogleWorkspaceCISASCuBAModel(
Provider=compliance.Provider.lower(),
Description=compliance.Description,
Domain="",
AssessmentDate=str(timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Attributes_Section=attribute.Section,
Requirements_Attributes_SubSection=attribute.SubSection,
Requirements_Attributes_Service=attribute.Service,
Requirements_Attributes_Type=attribute.Type,
Status="MANUAL",
StatusExtended="Manual check",
ResourceId="manual_check",
ResourceName="Manual check",
CheckId="manual",
Muted=False,
Framework=compliance.Framework,
Name=compliance.Name,
)
self._data.append(compliance_row)
@@ -1,28 +0,0 @@
from typing import Optional
from pydantic.v1 import BaseModel
class GoogleWorkspaceCISASCuBAModel(BaseModel):
"""
GoogleWorkspaceCISASCuBAModel generates a finding's output in Google Workspace CISA SCuBA Compliance format.
"""
Provider: str
Description: str
Domain: str
AssessmentDate: str
Requirements_Id: str
Requirements_Description: str
Requirements_Attributes_Section: Optional[str] = None
Requirements_Attributes_SubSection: Optional[str] = None
Requirements_Attributes_Service: Optional[str] = None
Requirements_Attributes_Type: Optional[str] = None
Status: str
StatusExtended: str
ResourceId: str
ResourceName: str
CheckId: str
Muted: bool
Framework: str
Name: str
-17
View File
@@ -404,23 +404,6 @@ class Finding(BaseModel):
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = check_output.zone_name
elif provider.type == "vercel":
output_data["auth_method"] = "api_token"
team = get_nested_attribute(provider, "identity.team")
output_data["account_uid"] = (
team.id
if team
else get_nested_attribute(provider, "identity.user_id")
)
output_data["account_name"] = (
team.name
if team
else get_nested_attribute(provider, "identity.username")
)
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = "global"
elif provider.type == "alibabacloud":
output_data["auth_method"] = get_nested_attribute(
provider, "identity.identity_arn"
+1 -67
View File
@@ -9,7 +9,6 @@ from prowler.config.config import (
square_logo_img,
timestamp,
)
from prowler.lib.cli.redact import redact_argv
from prowler.lib.logger import logger
from prowler.lib.outputs.output import Finding, Output
from prowler.lib.outputs.utils import parse_html_string, unroll_dict
@@ -197,7 +196,7 @@ class HTML(Output):
</div>
</li>
<li class="list-group-item">
<b>Parameters used:</b> {redact_argv(sys.argv[1:]) if from_cli else ""}
<b>Parameters used:</b> {" ".join(sys.argv[1:]) if from_cli else ""}
</li>
<li class="list-group-item">
<b>Date:</b> {timestamp.isoformat()}
@@ -1332,71 +1331,6 @@ class HTML(Output):
)
return ""
@staticmethod
def get_vercel_assessment_summary(provider: Provider) -> str:
"""
get_vercel_assessment_summary gets the HTML assessment summary for the Vercel provider
Args:
provider (Provider): the Vercel provider object
Returns:
str: HTML assessment summary for the Vercel provider
"""
try:
assessment_items = ""
team = getattr(provider.identity, "team", None)
if team:
assessment_items += f"""
<li class="list-group-item">
<b>Team:</b> {team.name} ({team.id})
</li>"""
credentials_items = """
<li class="list-group-item">
<b>Authentication:</b> API Token
</li>"""
email = getattr(provider.identity, "email", None)
if email:
credentials_items += f"""
<li class="list-group-item">
<b>Email:</b> {email}
</li>"""
username = getattr(provider.identity, "username", None)
if username:
credentials_items += f"""
<li class="list-group-item">
<b>Username:</b> {username}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Vercel Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Vercel Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_assessment_summary(provider: Provider) -> str:
"""
+1 -16
View File
@@ -44,11 +44,9 @@ class JiraConnection(Connection):
Represents a Jira connection object.
Attributes:
projects (dict): Dictionary of projects in Jira.
issue_types (dict): Dictionary of issue types per project key.
"""
projects: dict = None
issue_types: dict = None
class MarkdownToADFConverter:
@@ -783,20 +781,7 @@ class Jira:
)
projects = jira.get_projects()
issue_types = {}
for project_key in projects:
try:
issue_types[project_key] = jira.get_available_issue_types(
project_key
)
except Exception as e:
logger.warning(
f"Failed to get issue types for project {project_key}: {e}"
)
return JiraConnection(
is_connected=True, projects=projects, issue_types=issue_types
)
return JiraConnection(is_connected=True, projects=projects)
except JiraNoProjectsError as no_projects_error:
logger.error(
f"{no_projects_error.__class__.__name__}[{no_projects_error.__traceback__.tb_lineno}]: {no_projects_error}"
-2
View File
@@ -38,8 +38,6 @@ def stdout_report(finding, color, verbose, status, fix):
details = finding.zone_name
if finding.check_metadata.Provider == "googleworkspace":
details = finding.location
if finding.check_metadata.Provider == "vercel":
details = finding.region
if (verbose or fix) and (not status or finding.status in status):
if finding.muted:

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