Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ad98ed47e | |||
| 5306bb1133 | |||
| a7f18ec41f |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
# ============================================
|
||||
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,7 +60,6 @@ htmlcov/
|
||||
**/mcp-config.json
|
||||
**/mcpServers.json
|
||||
.mcp/
|
||||
.mcp.json
|
||||
|
||||
# AI Coding Assistants - Cursor
|
||||
.cursorignore
|
||||
|
||||
@@ -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 world’s most widely used _Open-Source Cloud Security Platform_ that automates security and compliance across **any cloud environment**. With hundreds of ready-to-use security checks, remediation guidance, and compliance frameworks, Prowler is built to _“Secure ANY Cloud at AI Speed”_. Prowler delivers **AI-driven**, **customizable**, and **easy-to-use** assessments, dashboards, reports, and integrations, making cloud security **simple**, **scalable**, and **cost-effective** for organizations of any size.
|
||||
**Prowler** is the world’s most widely used _open-source cloud security platform_ that automates security and compliance across **any cloud environment**. With hundreds of ready-to-use security checks, remediation guidance, and compliance frameworks, Prowler is built to _“Secure ANY cloud at AI Speed”_. Prowler delivers **AI-driven**, **customizable**, and **easy-to-use** assessments, dashboards, reports, and integrations, making cloud security **simple**, **scalable**, and **cost-effective** for organizations of any size.
|
||||
|
||||
Prowler 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.
|
||||
|
||||

|
||||
|
||||
<!-- Diagram source: docs/images/products/prowler-app-architecture.mmd — edit there, re-render at https://mermaid.live, and replace the PNG. -->
|
||||
|
||||

|
||||
|
||||
## Prowler CLI
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
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)
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 82 KiB |
@@ -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"
|
||||
|
||||
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
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
|
||||

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

|
||||
|
||||
3. Click "Add Cloud Provider"
|
||||
|
||||

|
||||
|
||||
4. Select "Container Registry"
|
||||
|
||||

|
||||
|
||||
5. Enter the container registry URL (e.g., `docker.io/myorg` or `myregistry.io`) and an optional alias, then click "Next"
|
||||
|
||||

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

|
||||
|
||||
### Step 3: Verify Connection & Start Scan
|
||||
|
||||
7. Review the provider configuration and click "Launch scan" to initiate the scan
|
||||
|
||||

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

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

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

|
||||
|
||||
### 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 |
|
||||
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 284 KiB |
|
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".
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
5. Verify in the apps list that the "User access" column displays **"ON for everyone"**.
|
||||
|
||||

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

|
||||
|
||||
</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`).
|
||||
|
||||

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

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

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

|
||||
|
||||
3. Fill in the user details (first name, last name, and primary email address in the configured domain).
|
||||
|
||||

|
||||
|
||||
4. Complete the user creation. Google Workspace generates temporary credentials for the new account.
|
||||
|
||||

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

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

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

|
||||
|
||||
### 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**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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:
|
||||
|
||||