mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-06 16:58:19 +00:00
Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44150687e1 | |||
| 9843d850e1 | |||
| 489c6c1073 | |||
| b08b072288 | |||
| ca29e354b6 | |||
| 85a3927950 | |||
| 04fe3f65e0 | |||
| 297c9d0734 | |||
| a2a1a73749 | |||
| 08fbe17e29 | |||
| d920f78059 | |||
| 12bf3d5e70 | |||
| 4002c28b5d | |||
| 2439f54280 | |||
| b0e59156e6 | |||
| f013bd4a53 | |||
| 6ad15f900f | |||
| 1784bf38ab | |||
| ba5b23245f | |||
| 43913b1592 | |||
| 9e31160887 | |||
| 9a0c73256e | |||
| 2a160a10df | |||
| 8d8bee165b | |||
| 606efec9f8 | |||
| d5354e8b1d | |||
| a96e5890dc | |||
| bb81c5dd2d | |||
| c3acb818d9 | |||
| e6fc59267b | |||
| 62f114f5d0 | |||
| 392ffd5a60 | |||
| 507b0882d5 | |||
| 89d72cf8fd | |||
| f3a042933f | |||
| 96e7d6cb3a | |||
| a82eaa885d | |||
| 90a619a8b4 | |||
| 638bf62d76 | |||
| 962615ca1f | |||
| 5610f5ad90 | |||
| be6fe1db04 | |||
| 92b838866a | |||
| 51591cb8cd | |||
| e24e1ab771 | |||
| bc3fd79457 | |||
| 4941ed5797 | |||
| 0f4d8ff891 | |||
| d1ab8b8ae5 | |||
| 65e9593b41 | |||
| 131112398b | |||
| c952ea018e | |||
| 31b645ee53 | |||
| 0123e603d8 | |||
| b65265da4b | |||
| 1335332fe9 | |||
| f37a2a1efe | |||
| 3e0e1398c4 | |||
| a4ad9ba01f | |||
| c6d5f44c5e | |||
| 5d24a41625 | |||
| e33825747f | |||
| d919d979dd | |||
| 6534faf678 | |||
| 1aa91cf60f | |||
| dad84f0ee2 | |||
| 0d7c5f6ac5 | |||
| 431776bcfd | |||
| 0e8080f09c | |||
| e4b2950436 | |||
| 63174caf98 | |||
| 4e508b69c9 | |||
| 18cfb191f5 | |||
| b898f257f1 | |||
| cccb3a4b94 | |||
| ca50b24d77 | |||
| 7eb204fff0 | |||
| 56c370d3a4 | |||
| b0d8534907 | |||
| ad36938717 | |||
| 10dd9460e9 | |||
| c8d41745dd | |||
| c6c000a369 | |||
| a2b083e8c8 | |||
| d2f7169537 | |||
| 632f2633c1 | |||
| 82d487a1e7 | |||
| 9a6a43637d | |||
| c21cf0ac20 | |||
| f3b142c0cf | |||
| eda90c4673 | |||
| def59a8cc2 | |||
| 1bfed74db5 | |||
| baf1194824 | |||
| b9270df3e6 | |||
| 379df7800d | |||
| fcabe1f99e | |||
| ad7a56d010 | |||
| 406eedd68a | |||
| bc38104903 | |||
| 9290d7e105 | |||
| 72e8f09c07 | |||
| 1d43885230 | |||
| e6aedcb207 | |||
| 89fe867944 | |||
| 2be2753c55 | |||
| 283259f34c | |||
| abaacd7dbf | |||
| 5e1e4bd8e4 | |||
| 33efd72b97 | |||
| b2788df8cc | |||
| b1b361af8b | |||
| 8bc03f8d04 | |||
| ca03d9c0a9 | |||
| 8985280621 | |||
| b7ee2b9690 | |||
| 6b2d9b5580 | |||
| c99ed991b7 | |||
| 7c0034524a | |||
| 749110de75 | |||
| 5fff3b920d | |||
| 961f9c86da | |||
| 0f1da703d1 | |||
| 07f3416493 | |||
| 509ec74c3d | |||
| ab8e83da3f | |||
| 6ac90eb1b5 | |||
| af6198e6c2 | |||
| dfe06a1077 |
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.16.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.25.0
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -13,11 +13,15 @@ inputs:
|
||||
poetry-version:
|
||||
description: 'Poetry version to install'
|
||||
required: false
|
||||
default: '2.1.1'
|
||||
default: '2.3.4'
|
||||
install-dependencies:
|
||||
description: 'Install Python dependencies with Poetry'
|
||||
required: false
|
||||
default: 'true'
|
||||
update-lock:
|
||||
description: 'Run `poetry lock` during setup. Only enable when a prior step mutates pyproject.toml (e.g. API `@master` VCS rewrite). Default: false.'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
@@ -74,7 +78,7 @@ runs:
|
||||
grep -A2 -B2 "resolved_reference" poetry.lock
|
||||
|
||||
- name: Update poetry.lock (prowler repo only)
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
if: github.repository == 'prowler-cloud/prowler' && inputs.update-lock == 'true'
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry lock
|
||||
|
||||
@@ -66,6 +66,18 @@ updates:
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "pre-commit"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 25
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pre-commit"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Dependabot Updates are temporary disabled - 2025/04/15
|
||||
# v4.6
|
||||
# - package-ecosystem: "pip"
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
PROWLER_VERSION: ${{ github.event.release.tag_name }}
|
||||
BASE_BRANCH: master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
detect-release-type:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -17,6 +17,8 @@ concurrency:
|
||||
env:
|
||||
API_WORKING_DIR: ./api
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
api-code-quality:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -50,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Check for API changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
api/**
|
||||
@@ -67,6 +69,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
working-directory: ./api
|
||||
update-lock: 'true'
|
||||
|
||||
- name: Poetry check
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -24,6 +24,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
api-analyze:
|
||||
name: CodeQL Security Analysis
|
||||
|
||||
@@ -33,6 +33,8 @@ env:
|
||||
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -137,18 +139,18 @@ jobs:
|
||||
sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build and push API container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
push: true
|
||||
@@ -178,7 +180,7 @@ jobs:
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -18,6 +18,8 @@ env:
|
||||
API_WORKING_DIR: ./api
|
||||
IMAGE_NAME: prowler-api
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
api-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -42,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Check if Dockerfile changed
|
||||
id: dockerfile-changed
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: api/Dockerfile
|
||||
|
||||
@@ -104,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Check for API changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: api/**
|
||||
files_ignore: |
|
||||
@@ -115,11 +117,11 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build container for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ${{ env.API_WORKING_DIR }}
|
||||
push: false
|
||||
|
||||
@@ -17,6 +17,8 @@ concurrency:
|
||||
env:
|
||||
API_WORKING_DIR: ./api
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
api-security-scans:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -53,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Check for API changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
api/**
|
||||
@@ -70,6 +72,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
working-directory: ./api
|
||||
update-lock: 'true'
|
||||
|
||||
- name: Bandit
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
@@ -77,9 +80,10 @@ jobs:
|
||||
|
||||
- name: Safety
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: poetry run safety check --ignore 79023,79027,86217
|
||||
run: poetry run safety check --ignore 79023,79027,86217,71600
|
||||
# TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0
|
||||
# TODO: 86217 because `alibabacloud-tea-openapi == 0.4.3` don't let us upgrade `cryptography >= 46.0.0`
|
||||
# TODO: 71600 CVE-2024-1135 false positive - fixed in gunicorn 22.0.0, project uses 23.0.0
|
||||
|
||||
- name: Vulture
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -30,6 +30,8 @@ env:
|
||||
VALKEY_DB: 0
|
||||
API_WORKING_DIR: ./api
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
api-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -99,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Check for API changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
api/**
|
||||
@@ -116,6 +118,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
working-directory: ./api
|
||||
update-lock: 'true'
|
||||
|
||||
- name: Run tests with pytest
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -17,6 +17,8 @@ env:
|
||||
BACKPORT_LABEL_PREFIX: backport-to-
|
||||
BACKPORT_LABEL_IGNORE: was-backported
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) && !(contains(github.event.pull_request.labels.*.name, 'was-backported'))
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Backport PR
|
||||
if: steps.label_check.outputs.label_check == 'success'
|
||||
uses: sorenlouv/backport-github-action@516854e7c9f962b9939085c9a92ea28411d1ae90 # v10.2.0
|
||||
uses: sorenlouv/backport-github-action@9460b7102fea25466026ce806c9ebf873ac48721 # v11.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX }}
|
||||
|
||||
@@ -21,6 +21,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -49,6 +51,6 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
|
||||
@@ -9,6 +9,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
update-labels:
|
||||
if: contains(github.event.issue.labels.*.name, 'status/awaiting-response')
|
||||
|
||||
@@ -16,6 +16,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
conventional-commit-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
BACKPORT_LABEL_PREFIX: backport-to-
|
||||
BACKPORT_LABEL_COLOR: B60205
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
create-label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
PROWLER_VERSION: ${{ github.event.release.tag_name }}
|
||||
BASE_BRANCH: master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
detect-release-type:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -14,6 +14,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
scan-secrets:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -21,6 +21,8 @@ concurrency:
|
||||
env:
|
||||
CHART_PATH: contrib/k8s/helm/prowler-app
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
helm-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -36,12 +38,12 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
|
||||
- name: Update chart dependencies
|
||||
run: helm dependency update ${{ env.CHART_PATH }}
|
||||
|
||||
@@ -13,6 +13,8 @@ concurrency:
|
||||
env:
|
||||
CHART_PATH: contrib/k8s/helm/prowler-app
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
release-helm-chart:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -29,12 +31,12 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
|
||||
- name: Set appVersion from release tag
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: 'Tools: Lock Issue on Close'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- closed
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
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);
|
||||
Generated
+9
-9
@@ -772,7 +772,7 @@ jobs:
|
||||
SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload Safe Outputs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: safe-output
|
||||
path: ${{ env.GH_AW_SAFE_OUTPUTS }}
|
||||
@@ -793,13 +793,13 @@ jobs:
|
||||
await main();
|
||||
- name: Upload sanitized agent output
|
||||
if: always() && env.GH_AW_AGENT_OUTPUT
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: agent-output
|
||||
path: ${{ env.GH_AW_AGENT_OUTPUT }}
|
||||
if-no-files-found: warn
|
||||
- name: Upload engine output files
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: agent_outputs
|
||||
path: |
|
||||
@@ -839,7 +839,7 @@ jobs:
|
||||
- name: Upload agent artifacts
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: agent-artifacts
|
||||
path: |
|
||||
@@ -880,7 +880,7 @@ jobs:
|
||||
destination: /opt/gh-aw/actions
|
||||
- name: Download agent output artifact
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: agent-output
|
||||
path: /tmp/gh-aw/safeoutputs/
|
||||
@@ -992,13 +992,13 @@ jobs:
|
||||
destination: /opt/gh-aw/actions
|
||||
- name: Download agent artifacts
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: agent-artifacts
|
||||
path: /tmp/gh-aw/threat-detection/
|
||||
- name: Download agent output artifact
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: agent-output
|
||||
path: /tmp/gh-aw/threat-detection/
|
||||
@@ -1071,7 +1071,7 @@ jobs:
|
||||
await main();
|
||||
- name: Upload threat detection log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: threat-detection.log
|
||||
path: /tmp/gh-aw/threat-detection/detection.log
|
||||
@@ -1174,7 +1174,7 @@ jobs:
|
||||
destination: /opt/gh-aw/actions
|
||||
- name: Download agent output artifact
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: agent-output
|
||||
path: /tmp/gh-aw/safeoutputs/
|
||||
|
||||
@@ -15,6 +15,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -76,6 +78,7 @@ jobs:
|
||||
"StylusFrost"
|
||||
"toniblyx"
|
||||
"davidm4r"
|
||||
"pfe-nazaries"
|
||||
)
|
||||
|
||||
echo "Checking if $AUTHOR is a member of prowler-cloud organization"
|
||||
|
||||
@@ -32,6 +32,8 @@ env:
|
||||
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-mcp
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -123,18 +125,18 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build and push MCP container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
push: true
|
||||
@@ -173,7 +175,7 @@ jobs:
|
||||
release-assets.githubusercontent.com:443
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -18,6 +18,8 @@ env:
|
||||
MCP_WORKING_DIR: ./mcp_server
|
||||
IMAGE_NAME: prowler-mcp
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
mcp-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -42,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Check if Dockerfile changed
|
||||
id: dockerfile-changed
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: mcp_server/Dockerfile
|
||||
|
||||
@@ -87,6 +89,9 @@ jobs:
|
||||
api.github.com:443
|
||||
mirror.gcr.io:443
|
||||
check.trivy.dev:443
|
||||
get.trivy.dev:443
|
||||
release-assets.githubusercontent.com:443
|
||||
objects.githubusercontent.com:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -96,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Check for MCP changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: mcp_server/**
|
||||
files_ignore: |
|
||||
@@ -105,11 +110,11 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build MCP container for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ${{ env.MCP_WORKING_DIR }}
|
||||
push: false
|
||||
|
||||
@@ -14,6 +14,8 @@ env:
|
||||
PYTHON_VERSION: "3.12"
|
||||
WORKING_DIRECTORY: ./mcp_server
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
validate-release:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
|
||||
@@ -16,6 +16,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-changelog:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'no-changelog') == false
|
||||
@@ -45,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
api/**
|
||||
|
||||
@@ -16,6 +16,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-compliance-mapping:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'no-compliance-check') == false
|
||||
@@ -43,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
prowler/providers/**/services/**/*.metadata.json
|
||||
|
||||
@@ -15,6 +15,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-conflicts:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -39,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: '**'
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
trigger-cloud-pull-request:
|
||||
if: |
|
||||
|
||||
@@ -17,6 +17,8 @@ concurrency:
|
||||
env:
|
||||
PROWLER_VERSION: ${{ inputs.prowler_version }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
if: github.event_name == 'workflow_dispatch' && github.repository == 'prowler-cloud/prowler'
|
||||
@@ -38,15 +40,11 @@ jobs:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Setup Python with Poetry
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
python3 -m pip install --user poetry==2.1.1
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
install-dependencies: 'false'
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@@ -380,7 +378,7 @@ jobs:
|
||||
no-changelog
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
tag_name: ${{ env.PROWLER_VERSION }}
|
||||
name: Prowler ${{ env.PROWLER_VERSION }}
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
PROWLER_VERSION: ${{ github.event.release.tag_name }}
|
||||
BASE_BRANCH: master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
detect-release-type:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -10,6 +10,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-duplicate-test-names:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
|
||||
@@ -14,6 +14,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sdk-code-quality:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Check for SDK changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
@@ -69,22 +71,11 @@ jobs:
|
||||
contrib/**
|
||||
**/AGENTS.md
|
||||
|
||||
- name: Install Poetry
|
||||
- name: Setup Python with Poetry
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pipx install poetry==2.1.1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry install --no-root
|
||||
poetry run pip list
|
||||
|
||||
- name: Check Poetry lock file
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -30,6 +30,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sdk-analyze:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
|
||||
@@ -47,6 +47,8 @@ env:
|
||||
# AWS configuration (for ECR)
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -74,15 +76,14 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Setup Python with Poetry
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
install-dependencies: 'false'
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pipx install poetry==2.1.1
|
||||
pipx inject poetry poetry-bumpversion
|
||||
- name: Inject poetry-bumpversion plugin
|
||||
run: pipx inject poetry poetry-bumpversion
|
||||
|
||||
- name: Get Prowler version and set tags
|
||||
id: get-prowler-version
|
||||
@@ -197,13 +198,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Public ECR
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
@@ -212,12 +213,12 @@ jobs:
|
||||
AWS_REGION: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build and push SDK container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKERFILE_PATH }}
|
||||
@@ -252,13 +253,13 @@ jobs:
|
||||
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Public ECR
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
@@ -295,7 +296,7 @@ jobs:
|
||||
# Push to toniblyx/prowler only for current version (latest/stable/release tags)
|
||||
- name: Login to DockerHub (toniblyx)
|
||||
if: needs.setup.outputs.latest_tag == 'latest'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.TONIBLYX_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.TONIBLYX_DOCKERHUB_PASSWORD }}
|
||||
@@ -320,7 +321,7 @@ jobs:
|
||||
# Re-login as prowlercloud for cleanup of intermediate tags
|
||||
- name: Login to DockerHub (prowlercloud)
|
||||
if: always()
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -17,6 +17,8 @@ concurrency:
|
||||
env:
|
||||
IMAGE_NAME: prowler
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sdk-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -41,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Check if Dockerfile changed
|
||||
id: dockerfile-changed
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: Dockerfile
|
||||
|
||||
@@ -85,6 +87,7 @@ jobs:
|
||||
check.trivy.dev:443
|
||||
debian.map.fastlydns.net:80
|
||||
release-assets.githubusercontent.com:443
|
||||
objects.githubusercontent.com:443
|
||||
pypi.org:443
|
||||
files.pythonhosted.org:443
|
||||
www.powershellgallery.com:443
|
||||
@@ -102,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Check for SDK changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
@@ -127,11 +130,11 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build SDK container for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
PYTHON_VERSION: '3.12'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
validate-release:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -73,13 +75,11 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Poetry
|
||||
run: pipx install poetry==2.1.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Setup Python with Poetry
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
install-dependencies: 'false'
|
||||
|
||||
- name: Build Prowler package
|
||||
run: poetry build
|
||||
@@ -111,13 +111,11 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Poetry
|
||||
run: pipx install poetry==2.1.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- name: Setup Python with Poetry
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
install-dependencies: 'false'
|
||||
|
||||
- name: Install toml package
|
||||
run: pip install toml
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
PYTHON_VERSION: '3.12'
|
||||
AWS_REGION: 'us-east-1'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
refresh-aws-regions:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
|
||||
@@ -12,6 +12,8 @@ concurrency:
|
||||
env:
|
||||
PYTHON_VERSION: '3.12'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
refresh-oci-regions:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
|
||||
@@ -14,6 +14,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sdk-security-scans:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -44,7 +46,7 @@ jobs:
|
||||
|
||||
- name: Check for SDK changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files:
|
||||
./**
|
||||
@@ -69,20 +71,11 @@ jobs:
|
||||
contrib/**
|
||||
**/AGENTS.md
|
||||
|
||||
- name: Install Poetry
|
||||
- name: Setup Python with Poetry
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pipx install poetry==2.1.1
|
||||
|
||||
- name: Set up Python 3.12
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: poetry install --no-root
|
||||
|
||||
- name: Security scan with Bandit
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -14,6 +14,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sdk-tests:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -67,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Check for SDK changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
@@ -90,26 +92,17 @@ jobs:
|
||||
contrib/**
|
||||
**/AGENTS.md
|
||||
|
||||
- name: Install Poetry
|
||||
- name: Setup Python with Poetry
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pipx install poetry==2.1.1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: ./.github/actions/setup-python-poetry
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: poetry install --no-root
|
||||
|
||||
# AWS Provider
|
||||
- name: Check if AWS files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-aws
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/aws/**
|
||||
@@ -216,11 +209,11 @@ jobs:
|
||||
echo "AWS service_paths='${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}'"
|
||||
|
||||
if [ "${STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL}" = "true" ]; then
|
||||
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
|
||||
poetry run pytest -p no:randomly -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 -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}
|
||||
poetry run pytest -p no:randomly -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 }}
|
||||
@@ -239,7 +232,7 @@ jobs:
|
||||
- name: Check if Azure files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-azure
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/azure/**
|
||||
@@ -263,7 +256,7 @@ jobs:
|
||||
- name: Check if GCP files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-gcp
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/gcp/**
|
||||
@@ -287,7 +280,7 @@ jobs:
|
||||
- name: Check if Kubernetes files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-kubernetes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/kubernetes/**
|
||||
@@ -311,7 +304,7 @@ jobs:
|
||||
- name: Check if GitHub files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-github
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/github/**
|
||||
@@ -335,7 +328,7 @@ jobs:
|
||||
- name: Check if NHN files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-nhn
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/nhn/**
|
||||
@@ -359,7 +352,7 @@ jobs:
|
||||
- name: Check if M365 files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-m365
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/m365/**
|
||||
@@ -383,7 +376,7 @@ jobs:
|
||||
- name: Check if IaC files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-iac
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/iac/**
|
||||
@@ -407,7 +400,7 @@ jobs:
|
||||
- name: Check if MongoDB Atlas files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-mongodbatlas
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/mongodbatlas/**
|
||||
@@ -431,7 +424,7 @@ jobs:
|
||||
- name: Check if OCI files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-oraclecloud
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/oraclecloud/**
|
||||
@@ -455,7 +448,7 @@ jobs:
|
||||
- name: Check if OpenStack files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-openstack
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/openstack/**
|
||||
@@ -479,7 +472,7 @@ jobs:
|
||||
- name: Check if Google Workspace files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-googleworkspace
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/googleworkspace/**
|
||||
@@ -503,7 +496,7 @@ jobs:
|
||||
- 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
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/vercel/**
|
||||
@@ -527,7 +520,7 @@ jobs:
|
||||
- name: Check if Lib files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-lib
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/lib/**
|
||||
@@ -551,7 +544,7 @@ jobs:
|
||||
- name: Check if Config files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-config
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/config/**
|
||||
|
||||
@@ -31,6 +31,8 @@ on:
|
||||
description: "Whether there are UI E2E tests to run"
|
||||
value: ${{ jobs.analyze.outputs.has-ui-e2e }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -66,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
PROWLER_VERSION: ${{ github.event.release.tag_name }}
|
||||
BASE_BRANCH: master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
detect-release-type:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -26,6 +26,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
ui-analyze:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
|
||||
@@ -35,6 +35,8 @@ env:
|
||||
# Build args
|
||||
NEXT_PUBLIC_API_BASE_URL: http://prowler-api:8080/api/v1
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -127,18 +129,18 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build and push UI container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
build-args: |
|
||||
@@ -172,7 +174,7 @@ jobs:
|
||||
production.cloudflare.docker.com:443
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -18,6 +18,8 @@ env:
|
||||
UI_WORKING_DIR: ./ui
|
||||
IMAGE_NAME: prowler-ui
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
ui-dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
@@ -42,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Check if Dockerfile changed
|
||||
id: dockerfile-changed
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: ui/Dockerfile
|
||||
|
||||
@@ -89,6 +91,8 @@ jobs:
|
||||
mirror.gcr.io:443
|
||||
check.trivy.dev:443
|
||||
get.trivy.dev:443
|
||||
release-assets.githubusercontent.com:443
|
||||
objects.githubusercontent.com:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -98,7 +102,7 @@ jobs:
|
||||
|
||||
- name: Check for UI changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: ui/**
|
||||
files_ignore: |
|
||||
@@ -108,11 +112,11 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build UI container for ${{ matrix.arch }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ${{ env.UI_WORKING_DIR }}
|
||||
target: prod
|
||||
|
||||
@@ -15,6 +15,8 @@ on:
|
||||
- 'ui/**'
|
||||
- 'api/**' # API changes can affect UI E2E
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
# First, analyze which tests need to run
|
||||
impact-analysis:
|
||||
@@ -158,21 +160,21 @@ jobs:
|
||||
'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '24.13.0'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
with:
|
||||
version: 10
|
||||
package_json_file: ui/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm and Next.js cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.STORE_PATH }}
|
||||
@@ -192,7 +194,7 @@ jobs:
|
||||
run: pnpm run build
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@@ -259,7 +261,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: failure()
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
@@ -18,6 +18,8 @@ env:
|
||||
UI_WORKING_DIR: ./ui
|
||||
NODE_VERSION: '24.13.0'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
ui-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -49,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Check for UI changes
|
||||
id: check-changes
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
ui/**
|
||||
@@ -62,7 +64,7 @@ jobs:
|
||||
- name: Get changed source files for targeted tests
|
||||
id: changed-source
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
ui/**/*.ts
|
||||
@@ -78,7 +80,7 @@ jobs:
|
||||
- name: Check for critical path changes (run all tests)
|
||||
id: critical-changes
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
with:
|
||||
files: |
|
||||
ui/lib/**
|
||||
@@ -90,15 +92,15 @@ jobs:
|
||||
|
||||
- name: Setup Node.js ${{ env.NODE_VERSION }}
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Setup pnpm
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
with:
|
||||
version: 10
|
||||
package_json_file: ui/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -108,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Setup pnpm and Next.js cache
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
${{ env.STORE_PATH }}
|
||||
|
||||
@@ -60,6 +60,7 @@ htmlcov/
|
||||
**/mcp-config.json
|
||||
**/mcpServers.json
|
||||
.mcp/
|
||||
.mcp.json
|
||||
|
||||
# AI Coding Assistants - Cursor
|
||||
.cursorignore
|
||||
@@ -83,6 +84,7 @@ continue.json
|
||||
.continuerc.json
|
||||
|
||||
# AI Coding Assistants - OpenCode
|
||||
.opencode/
|
||||
opencode.json
|
||||
|
||||
# AI Coding Assistants - GitHub Copilot
|
||||
|
||||
+12
-11
@@ -1,7 +1,7 @@
|
||||
repos:
|
||||
## GENERAL
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
@@ -16,7 +16,7 @@ repos:
|
||||
|
||||
## TOML
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.13.0
|
||||
rev: v2.16.0
|
||||
hooks:
|
||||
- id: pretty-format-toml
|
||||
args: [--autofix]
|
||||
@@ -24,21 +24,21 @@ repos:
|
||||
|
||||
## GITHUB ACTIONS
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.6.0
|
||||
rev: v1.24.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
files: ^\.github/
|
||||
|
||||
## BASH
|
||||
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||
rev: v0.10.0
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
exclude: contrib
|
||||
|
||||
## PYTHON
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v2.3.1
|
||||
rev: v2.3.3
|
||||
hooks:
|
||||
- id: autoflake
|
||||
exclude: ^skills/
|
||||
@@ -50,27 +50,27 @@ repos:
|
||||
]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
rev: 8.0.1
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ^skills/
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.4.2
|
||||
rev: 26.3.1
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: ^skills/
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.0.0
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: (contrib|^skills/)
|
||||
args: ["--ignore=E266,W503,E203,E501,W605"]
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 2.1.1
|
||||
rev: 2.3.4
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
name: API - poetry-check
|
||||
@@ -93,7 +93,7 @@ repos:
|
||||
pass_filenames: false
|
||||
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.13.0-beta
|
||||
rev: v2.14.0
|
||||
hooks:
|
||||
- id: hadolint
|
||||
args: ["--ignore=DL3013"]
|
||||
@@ -128,7 +128,8 @@ repos:
|
||||
# TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X
|
||||
# TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0
|
||||
# TODO: 86217 because `alibabacloud-tea-openapi == 0.4.3` don't let us upgrade `cryptography >= 46.0.0`
|
||||
entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,86217'
|
||||
# TODO: 71600 CVE-2024-1135 false positive - fixed in gunicorn 22.0.0, project uses 23.0.0
|
||||
entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,86217,71600'
|
||||
language: system
|
||||
|
||||
- id: vulture
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ build:
|
||||
post_create_environment:
|
||||
# Install poetry
|
||||
# https://python-poetry.org/docs/#installing-manually
|
||||
- python -m pip install poetry
|
||||
- python -m pip install poetry==2.3.4
|
||||
post_install:
|
||||
# Install dependencies with 'docs' dependency group
|
||||
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
|
||||
|
||||
@@ -140,7 +140,7 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure,
|
||||
|
||||
| Component | Location | Tech Stack |
|
||||
|-----------|----------|------------|
|
||||
| SDK | `prowler/` | Python 3.10+, Poetry |
|
||||
| SDK | `prowler/` | Python 3.10+, Poetry 2.3+ |
|
||||
| API | `api/` | Django 5.1, DRF, Celery |
|
||||
| UI | `ui/` | Next.js 15, React 19, Tailwind 4 |
|
||||
| MCP Server | `mcp_server/` | FastMCP, Python 3.12+ |
|
||||
@@ -153,12 +153,12 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure,
|
||||
```bash
|
||||
# Setup
|
||||
poetry install --with dev
|
||||
poetry run pre-commit install
|
||||
poetry run prek install
|
||||
|
||||
# Code quality
|
||||
poetry run make lint
|
||||
poetry run make format
|
||||
poetry run pre-commit run --all-files
|
||||
poetry run prek run --all-files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ ENV HOME='/home/prowler'
|
||||
ENV PATH="${HOME}/.local/bin:${PATH}"
|
||||
#hadolint ignore=DL3013
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir poetry
|
||||
pip install --no-cache-dir poetry==2.3.4
|
||||
|
||||
RUN poetry install --compile && \
|
||||
rm -rf ~/.cache/pip
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -246,14 +246,7 @@ 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).
|
||||
2. **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
|
||||
@@ -317,7 +310,10 @@ 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
|
||||
|
||||
|
||||
+44
-3
@@ -2,19 +2,55 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.24.0] (Prowler UNRELEASED)
|
||||
## [1.25.1] (Prowler v5.24.1)
|
||||
|
||||
### 🚀 Added
|
||||
### 🐞 Fixed
|
||||
|
||||
- `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)
|
||||
- Attack Paths: Missing `tenant_id` filter while getting related findings after scan completes [(#10722)](https://github.com/prowler-cloud/prowler/pull/10722)
|
||||
- Finding group counters `pass_count`, `fail_count` and `manual_count` now exclude muted findings [(#10753)](https://github.com/prowler-cloud/prowler/pull/10753)
|
||||
|
||||
---
|
||||
|
||||
## [1.25.0] (Prowler v5.24.0)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Bump Poetry to `2.3.4` in Dockerfile and pre-commit hooks. Regenerate `api/poetry.lock` [(#10681)](https://github.com/prowler-cloud/prowler/pull/10681)
|
||||
- Attack Paths: Remove dead `cleanup_findings` no-op and its supporting `prowler_finding_lastupdated` index [(#10684)](https://github.com/prowler-cloud/prowler/pull/10684)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Worker-beat race condition on cold start: replaced `sleep 15` with API service healthcheck dependency (Docker Compose) and init containers (Helm), aligned Gunicorn default port to `8080` [(#10603)](https://github.com/prowler-cloud/prowler/pull/10603)
|
||||
- API container startup crash on Linux due to root-owned bind-mount preventing JWT key generation [(#10646)](https://github.com/prowler-cloud/prowler/pull/10646)
|
||||
- Finding group resources endpoints now include findings without associated resources (orphan IaC findings) as simulated resource rows, and return one row per finding when multiple findings share a resource [(#10708)](https://github.com/prowler-cloud/prowler/pull/10708)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- `pytest` from 8.2.2 to 9.0.3 to fix CVE-2025-71176 [(#10678)](https://github.com/prowler-cloud/prowler/pull/10678)
|
||||
|
||||
---
|
||||
|
||||
## [1.24.0] (Prowler v5.23.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- RBAC role lookup filtered 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)
|
||||
- Finding group resources endpoints (`/finding-groups/{check_id}/resources` and `/finding-groups/latest/{check_id}/resources`) now expose `finding_id` per row, pointing to the most recent matching Finding for each resource. UUIDv7 ordering guarantees `Max(finding__id)` resolves to the latest snapshot [(#10630)](https://github.com/prowler-cloud/prowler/pull/10630)
|
||||
- Handle CIS and CISA SCuBA compliance framework from google workspace [(#10629)](https://github.com/prowler-cloud/prowler/pull/10629)
|
||||
- Sort support for all finding group counter fields: `pass_muted_count`, `fail_muted_count`, `manual_muted_count`, and all `new_*`/`changed_*` status-mute breakdown counters [(#10655)](https://github.com/prowler-cloud/prowler/pull/10655)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Finding groups list/latest/resources now expose `status` ∈ `{FAIL, PASS, MANUAL}` and `muted: bool` as orthogonal fields. The aggregated `status` reflects the underlying check outcome regardless of mute state, and `muted=true` signals that every finding in the group/resource is muted. New `manual_count` is exposed alongside `pass_count`/`fail_count`, plus `pass_muted_count`/`fail_muted_count`/`manual_muted_count` siblings so clients can isolate the muted half of each status. The `new_*`/`changed_*` deltas are now broken down by status and mute state via 12 new counters (`new_fail_count`, `new_fail_muted_count`, `new_pass_count`, `new_pass_muted_count`, `new_manual_count`, `new_manual_muted_count` and the matching `changed_*` set). New `filter[muted]=true|false` and `sort=status` (FAIL > PASS > MANUAL) / `sort=muted` are supported. `filter[status]=MUTED` is no longer accepted [(#10630)](https://github.com/prowler-cloud/prowler/pull/10630)
|
||||
- Attack Paths: Periodic cleanup of stale scans with dead-worker detection via Celery inspect, marking orphaned `EXECUTING` scans as `FAILED` and recovering `graph_data_ready` [(#10387)](https://github.com/prowler-cloud/prowler/pull/10387)
|
||||
- Attack Paths: Replace `_provider_id` property with `_Provider_{uuid}` label for provider isolation, add regex-based label injection for custom queries [(#10402)](https://github.com/prowler-cloud/prowler/pull/10402)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `reaggregate_all_finding_group_summaries_task` now refreshes finding group daily summaries for every `(provider, day)` combination instead of only the latest scan per provider, matching the unbounded scope of `mute_historical_findings_task`. Mute rule operations no longer leave older daily summaries drifting from the underlying muted findings [(#10630)](https://github.com/prowler-cloud/prowler/pull/10630)
|
||||
- Finding groups list/latest now apply computed status/severity filters and finding-level prefilters (delta, region, service, category, resource group, scan, resource type), plus `check_title` support for sort/filter consistency [(#10428)](https://github.com/prowler-cloud/prowler/pull/10428)
|
||||
- Populate compliance data inside `check_metadata` for findings, which was always returned as `null` [(#10449)](https://github.com/prowler-cloud/prowler/pull/10449)
|
||||
- 403 error for admin users listing tenants due to roles query not using the admin database connection [(#10460)](https://github.com/prowler-cloud/prowler/pull/10460)
|
||||
@@ -24,10 +60,15 @@ 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)
|
||||
- `aiohttp` bumped from 3.13.3 to 3.13.5 to fix CVE-2026-34520 (the C parser accepted null bytes and control characters in response headers) [(#10538)](https://github.com/prowler-cloud/prowler/pull/10538)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ RUN mkdir -p /tmp/prowler_api_output
|
||||
COPY pyproject.toml ./
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir poetry
|
||||
pip install --no-cache-dir poetry==2.3.4
|
||||
|
||||
ENV PATH="/home/prowler/.local/bin:$PATH"
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ start_worker() {
|
||||
|
||||
start_worker_beat() {
|
||||
echo "Starting the worker-beat..."
|
||||
sleep 15
|
||||
poetry run python -m celery -A config.celery beat -l "${DJANGO_LOGGING_LEVEL:-info}" --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
}
|
||||
|
||||
|
||||
Generated
+167
-139
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "about-time"
|
||||
@@ -103,132 +103,132 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.3"
|
||||
version = "3.13.5"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"},
|
||||
{file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"},
|
||||
{file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"},
|
||||
{file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"},
|
||||
{file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"},
|
||||
{file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"},
|
||||
{file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"},
|
||||
{file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"},
|
||||
{file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"},
|
||||
{file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"},
|
||||
{file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"},
|
||||
{file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"},
|
||||
{file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"},
|
||||
{file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"},
|
||||
{file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -943,14 +943,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.6.6"
|
||||
version = "1.6.9"
|
||||
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.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"},
|
||||
{file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"},
|
||||
{file = "authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3"},
|
||||
{file = "authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -6445,6 +6445,33 @@ docs = ["sphinx (>=1.7.1)"]
|
||||
redis = ["redis"]
|
||||
tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"]
|
||||
|
||||
[[package]]
|
||||
name = "prek"
|
||||
version = "0.3.9"
|
||||
description = "A Git hook manager written in Rust, designed as a drop-in alternative to pre-commit."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8"},
|
||||
{file = "prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1"},
|
||||
{file = "prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1"},
|
||||
{file = "prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3"},
|
||||
{file = "prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20"},
|
||||
{file = "prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4"},
|
||||
{file = "prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3"},
|
||||
{file = "prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958"},
|
||||
{file = "prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb"},
|
||||
{file = "prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929"},
|
||||
{file = "prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
@@ -6722,7 +6749,7 @@ uuid6 = "2024.7.10"
|
||||
type = "git"
|
||||
url = "https://github.com/prowler-cloud/prowler.git"
|
||||
reference = "master"
|
||||
resolved_reference = "2ddd5b3091bcdd8c7d44aba73b13c5c6f8f99e35"
|
||||
resolved_reference = "6ac90eb1b58590b6f2f51645dbef17b9231053f4"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
@@ -7129,14 +7156,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.11.0"
|
||||
version = "2.12.1"
|
||||
description = "JSON Web Token implementation in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"},
|
||||
{file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"},
|
||||
{file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"},
|
||||
{file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -7324,24 +7351,25 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.2.2"
|
||||
version = "9.0.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
|
||||
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
|
||||
{file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
|
||||
{file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2.0"
|
||||
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = ">=1.0.1"
|
||||
packaging = ">=22"
|
||||
pluggy = ">=1.5,<2"
|
||||
pygments = ">=2.7.2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-celery"
|
||||
@@ -9372,4 +9400,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "167d4549788b8bc8bb7772b9a81ade1eab73d8f354251a8d6af4901223cc7f67"
|
||||
content-hash = "077e89853cfe3a6d934841488cfa5a98ff6c92b71f74b817b71387d11559f143"
|
||||
|
||||
+3
-2
@@ -50,7 +50,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.24.0"
|
||||
version = "1.26.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
@@ -65,7 +65,7 @@ freezegun = "1.5.1"
|
||||
marshmallow = "==3.26.2"
|
||||
mypy = "1.10.1"
|
||||
pylint = "3.2.5"
|
||||
pytest = "8.2.2"
|
||||
pytest = "9.0.3"
|
||||
pytest-cov = "5.0.0"
|
||||
pytest-django = "4.8.0"
|
||||
pytest-env = "1.1.3"
|
||||
@@ -75,3 +75,4 @@ ruff = "0.5.0"
|
||||
safety = "3.7.0"
|
||||
tqdm = "4.67.1"
|
||||
vulture = "2.14"
|
||||
prek = "0.3.9"
|
||||
|
||||
@@ -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, Tenant
|
||||
from api.models import Role, UserRoleRelationship
|
||||
from api.rbac.permissions import HasPermissions
|
||||
|
||||
|
||||
@@ -113,27 +113,22 @@ class BaseTenantViewset(BaseViewSet):
|
||||
if request is not None:
|
||||
request.db_alias = self.db_alias
|
||||
|
||||
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
|
||||
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)
|
||||
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):
|
||||
Role.objects.using(MainRouter.admin_db).create(
|
||||
admin_role = Role.objects.using(MainRouter.admin_db).create(
|
||||
name="admin",
|
||||
tenant_id=tenant_id,
|
||||
manage_users=True,
|
||||
@@ -144,15 +139,11 @@ class BaseTenantViewset(BaseViewSet):
|
||||
manage_scans=True,
|
||||
unlimited_visibility=True,
|
||||
)
|
||||
|
||||
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
|
||||
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
|
||||
user=self.request.user,
|
||||
role=admin_role,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
if request.auth is None:
|
||||
|
||||
@@ -434,6 +434,7 @@ class ScanFilter(ProviderRelationshipFilterSet):
|
||||
class Meta:
|
||||
model = Scan
|
||||
fields = {
|
||||
"id": ["exact", "in"],
|
||||
"provider": ["exact", "in"],
|
||||
"name": ["exact", "icontains"],
|
||||
"started_at": ["gte", "lte"],
|
||||
@@ -1114,13 +1115,14 @@ class FindingGroupAggregatedComputedFilter(FilterSet):
|
||||
STATUS_CHOICES = (
|
||||
("FAIL", "Fail"),
|
||||
("PASS", "Pass"),
|
||||
("MUTED", "Muted"),
|
||||
("MANUAL", "Manual"),
|
||||
)
|
||||
|
||||
status = ChoiceFilter(method="filter_status", choices=STATUS_CHOICES)
|
||||
status__in = CharInFilter(method="filter_status_in", lookup_expr="in")
|
||||
severity = ChoiceFilter(method="filter_severity", choices=SeverityChoices)
|
||||
severity__in = CharInFilter(method="filter_severity_in", lookup_expr="in")
|
||||
muted = BooleanFilter(field_name="muted")
|
||||
include_muted = BooleanFilter(method="filter_include_muted")
|
||||
|
||||
def filter_status(self, queryset, name, value):
|
||||
@@ -1197,7 +1199,7 @@ class FindingGroupAggregatedComputedFilter(FilterSet):
|
||||
if value is True:
|
||||
return queryset
|
||||
# include_muted=false: exclude fully-muted groups
|
||||
return queryset.exclude(fail_count=0, pass_count=0, muted_count__gt=0)
|
||||
return queryset.exclude(muted=True)
|
||||
|
||||
|
||||
class ProviderSecretFilter(FilterSet):
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
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,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,95 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0087_vercel_provider"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="manual_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="pass_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="fail_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="manual_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="muted",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="new_fail_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="new_fail_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="new_pass_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="new_pass_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="new_manual_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="new_manual_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="changed_fail_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="changed_fail_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="changed_pass_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="changed_pass_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="changed_manual_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="findinggroupdailysummary",
|
||||
name="changed_manual_muted_count",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
from django.db import migrations
|
||||
from tasks.tasks import backfill_finding_group_summaries_task
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
def trigger_backfill_task(apps, schema_editor):
|
||||
"""
|
||||
Re-dispatch the finding-group backfill task for every tenant so the new
|
||||
`manual_count` and `muted` columns added in 0088 get populated from the
|
||||
last 10 days of completed scans.
|
||||
|
||||
The aggregator (`aggregate_finding_group_summaries`) recomputes every
|
||||
column on each call, so it back-populates the new fields without touching
|
||||
the existing ones beyond a normal upsert.
|
||||
"""
|
||||
tenant_ids = Tenant.objects.using(MainRouter.admin_db).values_list("id", flat=True)
|
||||
|
||||
for tenant_id in tenant_ids:
|
||||
backfill_finding_group_summaries_task.delay(tenant_id=str(tenant_id), days=10)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0088_finding_group_status_muted_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(trigger_backfill_task, migrations.RunPython.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,6 +295,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
OPENSTACK = "openstack", _("OpenStack")
|
||||
IMAGE = "image", _("Image")
|
||||
GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace")
|
||||
VERCEL = "vercel", _("Vercel")
|
||||
|
||||
@staticmethod
|
||||
def validate_aws_uid(value):
|
||||
@@ -438,6 +439,15 @@ 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):
|
||||
@@ -1738,15 +1748,45 @@ class FindingGroupDailySummary(RowLevelSecurityProtectedModel):
|
||||
# Severity stored as integer for MAX aggregation (5=critical, 4=high, etc.)
|
||||
severity_order = models.SmallIntegerField(default=1)
|
||||
|
||||
# Finding counts
|
||||
# Finding counts (inclusive of muted findings; use the `muted` flag to
|
||||
# tell whether the group has any actionable findings).
|
||||
pass_count = models.IntegerField(default=0)
|
||||
fail_count = models.IntegerField(default=0)
|
||||
manual_count = models.IntegerField(default=0)
|
||||
muted_count = models.IntegerField(default=0)
|
||||
|
||||
# Delta counts
|
||||
# Status counts restricted to muted findings, so clients can isolate the
|
||||
# muted half of each status (e.g. `pass_count - pass_muted_count` gives the
|
||||
# actionable PASS findings).
|
||||
pass_muted_count = models.IntegerField(default=0)
|
||||
fail_muted_count = models.IntegerField(default=0)
|
||||
manual_muted_count = models.IntegerField(default=0)
|
||||
|
||||
# Whether every finding for this (provider, check, day) is muted.
|
||||
muted = models.BooleanField(default=False)
|
||||
|
||||
# Delta counts (non-muted, kept for convenience and as a "total" view).
|
||||
new_count = models.IntegerField(default=0)
|
||||
changed_count = models.IntegerField(default=0)
|
||||
|
||||
# Delta breakdown by (status, muted) so clients can answer questions like
|
||||
# "how many new failing findings appeared in this scan?" without scanning
|
||||
# the underlying findings table. Mirrors the existing pass/fail/manual
|
||||
# naming, with `_muted_count` siblings tracking the muted half of each
|
||||
# bucket explicitly.
|
||||
new_fail_count = models.IntegerField(default=0)
|
||||
new_fail_muted_count = models.IntegerField(default=0)
|
||||
new_pass_count = models.IntegerField(default=0)
|
||||
new_pass_muted_count = models.IntegerField(default=0)
|
||||
new_manual_count = models.IntegerField(default=0)
|
||||
new_manual_muted_count = models.IntegerField(default=0)
|
||||
changed_fail_count = models.IntegerField(default=0)
|
||||
changed_fail_muted_count = models.IntegerField(default=0)
|
||||
changed_pass_count = models.IntegerField(default=0)
|
||||
changed_pass_muted_count = models.IntegerField(default=0)
|
||||
changed_manual_count = models.IntegerField(default=0)
|
||||
changed_manual_muted_count = models.IntegerField(default=0)
|
||||
|
||||
# Resource counts
|
||||
resources_fail = models.IntegerField(default=0)
|
||||
resources_total = models.IntegerField(default=0)
|
||||
|
||||
@@ -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,11 +29,17 @@ 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)
|
||||
.all()
|
||||
.filter(tenant_id=tenant_id)
|
||||
)
|
||||
if not user_roles:
|
||||
return False
|
||||
@@ -45,14 +51,17 @@ class HasPermissions(BasePermission):
|
||||
return True
|
||||
|
||||
|
||||
def get_role(user: User) -> Optional[Role]:
|
||||
def get_role(user: User, tenant_id: str) -> Role:
|
||||
"""
|
||||
Retrieve the first role assigned to the given user.
|
||||
Retrieve the role assigned to the given user in the specified tenant.
|
||||
|
||||
Returns:
|
||||
The user's first Role instance if the user has any roles, otherwise None.
|
||||
Raises:
|
||||
PermissionDenied: If the user has no role in the given tenant.
|
||||
"""
|
||||
return user.roles.first()
|
||||
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
|
||||
|
||||
|
||||
def get_providers(role: Role) -> QuerySet[Provider]:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.24.0
|
||||
version: 1.26.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -372,6 +372,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -387,6 +388,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -409,6 +411,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -426,6 +429,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -1351,6 +1355,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -1366,6 +1371,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -1827,6 +1833,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -1842,6 +1849,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -1864,6 +1872,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -1881,6 +1890,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -2429,6 +2439,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -2444,6 +2455,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -2466,6 +2478,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -2483,6 +2496,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -2939,6 +2953,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -2954,6 +2969,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -2976,6 +2992,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -2993,6 +3010,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -3447,6 +3465,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -3462,6 +3481,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -3484,6 +3504,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -3501,6 +3522,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -3943,6 +3965,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -3958,6 +3981,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -3980,6 +4004,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -3997,6 +4022,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -5780,6 +5806,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -5795,6 +5822,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5817,6 +5845,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -5834,6 +5863,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -5955,6 +5985,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -5970,6 +6001,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5992,6 +6024,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -6009,6 +6042,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -6119,6 +6153,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -6134,6 +6169,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6155,6 +6191,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -6172,6 +6209,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -6314,6 +6352,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -6329,6 +6368,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6351,6 +6391,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -6368,6 +6409,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -6523,6 +6565,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -6538,6 +6581,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6560,6 +6604,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -6577,6 +6622,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -6726,6 +6772,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -6741,6 +6788,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6762,6 +6810,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -6779,6 +6828,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -6970,6 +7020,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -6985,6 +7036,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -7007,6 +7059,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -7024,6 +7077,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -7144,6 +7198,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -7159,6 +7214,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -7181,6 +7237,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -7198,6 +7255,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -7342,6 +7400,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -7357,6 +7416,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -7379,6 +7439,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -7396,6 +7457,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -8181,6 +8243,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -8196,6 +8259,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider__in]
|
||||
schema:
|
||||
@@ -8218,6 +8282,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -8235,6 +8300,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -8257,6 +8323,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -8272,6 +8339,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -8294,6 +8362,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -8311,6 +8380,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -8980,6 +9050,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -8995,6 +9066,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -9017,6 +9089,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -9034,6 +9107,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -9527,6 +9601,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -9542,6 +9617,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -9564,6 +9640,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -9581,6 +9658,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -9887,6 +9965,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -9902,6 +9981,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -9924,6 +10004,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -9941,6 +10022,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -10253,6 +10335,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -10268,6 +10351,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -10290,6 +10374,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -10307,6 +10392,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -11129,6 +11215,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
@@ -11144,6 +11231,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -11166,6 +11254,7 @@ paths:
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
- vercel
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
@@ -11183,6 +11272,7 @@ paths:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -18463,6 +18553,15 @@ 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
|
||||
@@ -19465,6 +19564,7 @@ components:
|
||||
- openstack
|
||||
- image
|
||||
- googleworkspace
|
||||
- vercel
|
||||
type: string
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -19481,6 +19581,7 @@ components:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
x-spec-enum-id: c0d56cad8ab9abe5
|
||||
uid:
|
||||
type: string
|
||||
@@ -19601,6 +19702,7 @@ components:
|
||||
- openstack
|
||||
- image
|
||||
- googleworkspace
|
||||
- vercel
|
||||
type: string
|
||||
x-spec-enum-id: c0d56cad8ab9abe5
|
||||
description: |-
|
||||
@@ -19620,6 +19722,7 @@ components:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
@@ -19671,6 +19774,7 @@ components:
|
||||
- openstack
|
||||
- image
|
||||
- googleworkspace
|
||||
- vercel
|
||||
type: string
|
||||
x-spec-enum-id: c0d56cad8ab9abe5
|
||||
description: |-
|
||||
@@ -19690,6 +19794,7 @@ components:
|
||||
* `openstack` - OpenStack
|
||||
* `image` - Image
|
||||
* `googleworkspace` - Google Workspace
|
||||
* `vercel` - Vercel
|
||||
uid:
|
||||
type: string
|
||||
minLength: 3
|
||||
@@ -20539,6 +20644,15 @@ 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
|
||||
@@ -20955,6 +21069,15 @@ 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
|
||||
@@ -21381,6 +21504,15 @@ 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,6 +215,21 @@ 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 TODAY
|
||||
from conftest import TEST_PASSWORD, TODAY
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
@@ -830,3 +830,66 @@ 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,6 +33,7 @@ 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:
|
||||
@@ -128,6 +129,7 @@ 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):
|
||||
@@ -218,6 +220,24 @@ 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
|
||||
@@ -284,6 +304,10 @@ 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):
|
||||
@@ -782,11 +806,15 @@ class TestProwlerIntegrationConnectionTest:
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock successful JIRA connection with projects
|
||||
# Mock successful JIRA connection with projects and issue types
|
||||
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
|
||||
@@ -815,6 +843,12 @@ 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()
|
||||
|
||||
@@ -838,6 +872,7 @@ 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
|
||||
@@ -863,6 +898,9 @@ 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()
|
||||
|
||||
@@ -881,11 +919,11 @@ class TestProwlerIntegrationConnectionTest:
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {
|
||||
"issue_types": ["Task"], # Existing configuration
|
||||
"issue_types": {"OLD_PROJ": ["Task"]}, # Existing configuration
|
||||
"projects": {"OLD_PROJ": "Old Project"}, # Will be overwritten
|
||||
}
|
||||
|
||||
# Mock successful JIRA connection with new projects
|
||||
# Mock successful JIRA connection with new projects and issue types
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
@@ -893,6 +931,10 @@ 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
|
||||
@@ -910,8 +952,11 @@ class TestProwlerIntegrationConnectionTest:
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
|
||||
# Verify other configuration fields were preserved
|
||||
assert integration.configuration["issue_types"] == ["Task"]
|
||||
# Verify issue types were also updated
|
||||
assert integration.configuration["issue_types"] == {
|
||||
"NEW_PROJ1": ["Task", "Bug"],
|
||||
"NEW_PROJ2": ["Story"],
|
||||
}
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@@ -57,6 +57,7 @@ from api.models import (
|
||||
ProviderGroupMembership,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceFindingMapping,
|
||||
Role,
|
||||
RoleProviderGroupRelationship,
|
||||
SAMLConfiguration,
|
||||
@@ -516,6 +517,13 @@ 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(
|
||||
@@ -575,22 +583,66 @@ 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
|
||||
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
|
||||
# user2 and user3 are deleted (no other memberships), test_user remains
|
||||
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):
|
||||
@@ -694,7 +746,6 @@ 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
|
||||
):
|
||||
@@ -807,6 +858,30 @@ 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
|
||||
):
|
||||
@@ -1715,6 +1790,46 @@ 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
|
||||
(
|
||||
{
|
||||
@@ -1918,21 +2033,21 @@ class TestProviderViewSet:
|
||||
(
|
||||
"uid.icontains",
|
||||
"1",
|
||||
11,
|
||||
12,
|
||||
),
|
||||
("alias", "aws_testing_1", 1),
|
||||
("alias.icontains", "aws", 2),
|
||||
("inserted_at", TODAY, 12),
|
||||
("inserted_at", TODAY, 13),
|
||||
(
|
||||
"inserted_at.gte",
|
||||
"2024-01-01",
|
||||
12,
|
||||
13,
|
||||
),
|
||||
("inserted_at.lte", "2024-01-01", 0),
|
||||
(
|
||||
"updated_at.gte",
|
||||
"2024-01-01",
|
||||
12,
|
||||
13,
|
||||
),
|
||||
("updated_at.lte", "2024-01-01", 0),
|
||||
]
|
||||
@@ -2557,6 +2672,14 @@ 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(
|
||||
@@ -3235,6 +3358,29 @@ 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
|
||||
@@ -4142,7 +4288,6 @@ 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(
|
||||
@@ -4169,7 +4314,6 @@ 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"]
|
||||
@@ -4224,7 +4368,6 @@ 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(
|
||||
@@ -4308,7 +4451,6 @@ 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"
|
||||
),
|
||||
@@ -4363,7 +4505,6 @@ 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"
|
||||
),
|
||||
@@ -4443,7 +4584,6 @@ class TestAttackPathsScanViewSet:
|
||||
"truncated": False,
|
||||
},
|
||||
),
|
||||
patch("api.v1.views.graph_database.clear_cache"),
|
||||
):
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
@@ -4509,7 +4649,6 @@ 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(
|
||||
@@ -4566,7 +4705,6 @@ 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(
|
||||
@@ -4613,7 +4751,6 @@ 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(
|
||||
@@ -4964,9 +5101,6 @@ 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(
|
||||
@@ -8097,6 +8231,8 @@ 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(
|
||||
@@ -15310,10 +15446,16 @@ class TestFindingGroupViewSet:
|
||||
# iam_password_policy has only PASS findings
|
||||
assert data[0]["attributes"]["status"] == "PASS"
|
||||
|
||||
def test_finding_groups_status_muted_all(
|
||||
def test_finding_groups_fully_muted_group_reflects_underlying_status(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""Test that MUTED status returned when all findings are muted."""
|
||||
"""A fully-muted group still surfaces its underlying status (no MUTED).
|
||||
|
||||
rds_encryption has 2 muted FAIL findings, so the group must report
|
||||
status=FAIL (the orthogonal `muted` boolean signals it isn't actionable).
|
||||
The status×muted breakdown lets clients answer 'how many failing
|
||||
findings are muted in this group'.
|
||||
"""
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-group-list"),
|
||||
{"filter[inserted_at]": TODAY, "filter[check_id]": "rds_encryption"},
|
||||
@@ -15321,8 +15463,21 @@ class TestFindingGroupViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 1
|
||||
# rds_encryption has all muted findings
|
||||
assert data[0]["attributes"]["status"] == "MUTED"
|
||||
attrs = data[0]["attributes"]
|
||||
assert attrs["status"] == "FAIL"
|
||||
assert attrs["muted"] is True
|
||||
assert attrs["fail_count"] == 0
|
||||
assert attrs["fail_muted_count"] == 2
|
||||
assert attrs["pass_muted_count"] == 0
|
||||
assert attrs["manual_muted_count"] == 0
|
||||
assert attrs["muted_count"] == 2
|
||||
# Sanity: the per-status muted counts must add up to muted_count.
|
||||
assert (
|
||||
attrs["pass_muted_count"]
|
||||
+ attrs["fail_muted_count"]
|
||||
+ attrs["manual_muted_count"]
|
||||
== attrs["muted_count"]
|
||||
)
|
||||
|
||||
def test_finding_groups_status_filter(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
@@ -15814,7 +15969,7 @@ class TestFindingGroupViewSet:
|
||||
"extra_filters",
|
||||
[
|
||||
{},
|
||||
{"filter[muted]": "include"},
|
||||
{"filter[delta]": "new"},
|
||||
],
|
||||
ids=["summary_path", "finding_level_path"],
|
||||
)
|
||||
@@ -15832,7 +15987,8 @@ class TestFindingGroupViewSet:
|
||||
|
||||
Parametrized to cover both aggregation paths:
|
||||
- summary_path: default, uses _CheckTitleToCheckIdMixin on summaries
|
||||
- finding_level_path: filter[muted]=include forces CommonFindingFilters
|
||||
- finding_level_path: filter[delta]=new forces _aggregate_findings via
|
||||
CommonFindingFilters (delta is finding-level, not summary-level)
|
||||
"""
|
||||
params = {
|
||||
"filter[inserted_at]": TODAY,
|
||||
@@ -15875,6 +16031,36 @@ class TestFindingGroupViewSet:
|
||||
# s3_bucket_public_access has 2 findings with 2 different resources
|
||||
assert len(data) == 2
|
||||
|
||||
def test_resources_id_matches_resource_id_for_mapped_findings(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""Findings with a resource expose the resource id as row id (hot path contract)."""
|
||||
response = authenticated_client.get(
|
||||
reverse(
|
||||
"finding-group-resources", kwargs={"pk": "s3_bucket_public_access"}
|
||||
),
|
||||
{"filter[inserted_at]": TODAY},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data, "expected resources in response"
|
||||
|
||||
resource_ids = set(
|
||||
ResourceFindingMapping.objects.filter(
|
||||
finding__check_id="s3_bucket_public_access",
|
||||
).values_list("resource_id", flat=True)
|
||||
)
|
||||
finding_ids = set(
|
||||
Finding.objects.filter(
|
||||
check_id="s3_bucket_public_access",
|
||||
).values_list("id", flat=True)
|
||||
)
|
||||
|
||||
returned_ids = {item["id"] for item in data}
|
||||
assert returned_ids <= {str(rid) for rid in resource_ids}
|
||||
assert returned_ids.isdisjoint({str(fid) for fid in finding_ids})
|
||||
|
||||
def test_resources_fields(self, authenticated_client, finding_groups_fixture):
|
||||
"""Test resource fields (uid, name, service, region, type) have valid values."""
|
||||
response = authenticated_client.get(
|
||||
@@ -16704,6 +16890,39 @@ 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
|
||||
):
|
||||
@@ -16717,3 +16936,338 @@ class TestFindingGroupViewSet:
|
||||
data = response.json()["data"]
|
||||
# Should still return data, not filtered by the old date
|
||||
assert len(data) == 5
|
||||
|
||||
def test_finding_groups_status_choices_no_muted(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""Every returned group must have status ∈ {FAIL, PASS, MANUAL}."""
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-group-list"),
|
||||
{"filter[inserted_at]": TODAY},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
statuses = {item["attributes"]["status"] for item in response.json()["data"]}
|
||||
assert statuses, "fixture should produce at least one group"
|
||||
assert statuses <= {"FAIL", "PASS", "MANUAL"}
|
||||
assert "MUTED" not in statuses
|
||||
|
||||
def test_finding_groups_serializer_exposes_muted_and_manual_count(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""The /finding-groups payload must expose `muted`, `manual_count` and
|
||||
the per-status muted siblings (`pass_muted_count`/`fail_muted_count`/
|
||||
`manual_muted_count`)."""
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-group-list"),
|
||||
{"filter[inserted_at]": TODAY, "filter[check_id]": "iam_password_policy"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
attrs = response.json()["data"][0]["attributes"]
|
||||
assert "muted" in attrs and isinstance(attrs["muted"], bool)
|
||||
assert "manual_count" in attrs and isinstance(attrs["manual_count"], int)
|
||||
assert attrs["muted"] is False # iam_password_policy has only non-muted PASS
|
||||
assert attrs["manual_count"] == 0
|
||||
assert attrs["pass_muted_count"] == 0
|
||||
assert attrs["fail_muted_count"] == 0
|
||||
assert attrs["manual_muted_count"] == 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
def test_finding_groups_filter_status_muted_is_rejected(
|
||||
self, authenticated_client, finding_groups_fixture, endpoint_name
|
||||
):
|
||||
"""`filter[status]=MUTED` is no longer a valid status value."""
|
||||
params = {"filter[status]": "MUTED"}
|
||||
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_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
def test_finding_groups_filter_muted_true(
|
||||
self, authenticated_client, finding_groups_fixture, endpoint_name
|
||||
):
|
||||
"""`filter[muted]=true` returns only fully-muted groups."""
|
||||
params = {"filter[muted]": "true"}
|
||||
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"]
|
||||
check_ids = {item["id"] for item in data}
|
||||
# Only rds_encryption is fully muted in the fixture
|
||||
assert check_ids == {"rds_encryption"}
|
||||
assert all(item["attributes"]["muted"] is True for item in data)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
def test_finding_groups_filter_muted_false(
|
||||
self, authenticated_client, finding_groups_fixture, endpoint_name
|
||||
):
|
||||
"""`filter[muted]=false` returns only groups with actionable findings."""
|
||||
params = {"filter[muted]": "false"}
|
||||
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"]
|
||||
check_ids = {item["id"] for item in data}
|
||||
assert "rds_encryption" not in check_ids
|
||||
assert check_ids == {
|
||||
"s3_bucket_public_access",
|
||||
"ec2_instance_public_ip",
|
||||
"iam_password_policy",
|
||||
"cloudtrail_enabled",
|
||||
}
|
||||
assert all(item["attributes"]["muted"] is False for item in data)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
def test_finding_groups_sort_by_status(
|
||||
self, authenticated_client, finding_groups_fixture, endpoint_name
|
||||
):
|
||||
"""sort=status orders by aggregated status (FAIL > PASS > MANUAL)."""
|
||||
priority = {"FAIL": 3, "PASS": 2, "MANUAL": 1}
|
||||
params = {"sort": "-status"}
|
||||
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 data, "fixture should produce groups"
|
||||
|
||||
desc_keys = [priority[item["attributes"]["status"]] for item in data]
|
||||
assert desc_keys == sorted(desc_keys, reverse=True)
|
||||
|
||||
params["sort"] = "status"
|
||||
response = authenticated_client.get(reverse(endpoint_name), params)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
asc_keys = [
|
||||
priority[item["attributes"]["status"]] for item in response.json()["data"]
|
||||
]
|
||||
assert asc_keys == sorted(asc_keys)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
def test_finding_groups_sort_by_muted(
|
||||
self, authenticated_client, finding_groups_fixture, endpoint_name
|
||||
):
|
||||
"""sort=muted orders by the boolean muted attribute."""
|
||||
# Need include_muted=true so the fully-muted group is part of the result
|
||||
params = {"sort": "-muted", "filter[include_muted]": "true"}
|
||||
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 data, "fixture should produce groups"
|
||||
|
||||
muted_values = [item["attributes"]["muted"] for item in data]
|
||||
# Descending boolean: True (1) before False (0)
|
||||
assert muted_values == sorted(muted_values, reverse=True)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"sort_field",
|
||||
[
|
||||
"pass_muted_count",
|
||||
"fail_muted_count",
|
||||
"manual_muted_count",
|
||||
"new_fail_count",
|
||||
"new_fail_muted_count",
|
||||
"new_pass_count",
|
||||
"new_pass_muted_count",
|
||||
"new_manual_count",
|
||||
"new_manual_muted_count",
|
||||
"changed_fail_count",
|
||||
"changed_fail_muted_count",
|
||||
"changed_pass_count",
|
||||
"changed_pass_muted_count",
|
||||
"changed_manual_count",
|
||||
"changed_manual_muted_count",
|
||||
],
|
||||
)
|
||||
def test_finding_groups_sort_by_counter_fields(
|
||||
self,
|
||||
authenticated_client,
|
||||
finding_groups_fixture,
|
||||
endpoint_name,
|
||||
sort_field,
|
||||
):
|
||||
"""All counter fields are accepted as sort parameters (asc and desc)."""
|
||||
params = {"sort": f"-{sort_field}"}
|
||||
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
|
||||
|
||||
desc_values = [item["attributes"][sort_field] for item in data]
|
||||
assert desc_values == sorted(desc_values, reverse=True)
|
||||
|
||||
params["sort"] = sort_field
|
||||
response = authenticated_client.get(reverse(endpoint_name), params)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
asc_values = [
|
||||
item["attributes"][sort_field] for item in response.json()["data"]
|
||||
]
|
||||
assert asc_values == sorted(asc_values)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_name", ["finding-group-list", "finding-group-latest"]
|
||||
)
|
||||
def test_finding_groups_delta_status_breakdown(
|
||||
self, authenticated_client, finding_groups_fixture, endpoint_name
|
||||
):
|
||||
"""`new_*` and `changed_*` counters split by status and mute state.
|
||||
|
||||
s3_bucket_public_access has 1 new FAIL and 1 changed FAIL (both
|
||||
non-muted) so the breakdown must reflect exactly that and the totals
|
||||
must equal the sum of the buckets.
|
||||
"""
|
||||
params = {"filter[check_id]": "s3_bucket_public_access"}
|
||||
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) == 1
|
||||
attrs = data[0]["attributes"]
|
||||
|
||||
assert attrs["new_fail_count"] == 1
|
||||
assert attrs["new_fail_muted_count"] == 0
|
||||
assert attrs["new_pass_count"] == 0
|
||||
assert attrs["new_pass_muted_count"] == 0
|
||||
assert attrs["new_manual_count"] == 0
|
||||
assert attrs["new_manual_muted_count"] == 0
|
||||
assert attrs["changed_fail_count"] == 1
|
||||
assert attrs["changed_fail_muted_count"] == 0
|
||||
assert attrs["changed_pass_count"] == 0
|
||||
assert attrs["changed_pass_muted_count"] == 0
|
||||
assert attrs["changed_manual_count"] == 0
|
||||
assert attrs["changed_manual_muted_count"] == 0
|
||||
|
||||
new_total = (
|
||||
attrs["new_fail_count"]
|
||||
+ attrs["new_fail_muted_count"]
|
||||
+ attrs["new_pass_count"]
|
||||
+ attrs["new_pass_muted_count"]
|
||||
+ attrs["new_manual_count"]
|
||||
+ attrs["new_manual_muted_count"]
|
||||
)
|
||||
changed_total = (
|
||||
attrs["changed_fail_count"]
|
||||
+ attrs["changed_fail_muted_count"]
|
||||
+ attrs["changed_pass_count"]
|
||||
+ attrs["changed_pass_muted_count"]
|
||||
+ attrs["changed_manual_count"]
|
||||
+ attrs["changed_manual_muted_count"]
|
||||
)
|
||||
# The non-muted variants of the breakdown must sum to the legacy
|
||||
# totals (new_count/changed_count are stored as non-muted).
|
||||
assert (
|
||||
attrs["new_fail_count"]
|
||||
+ attrs["new_pass_count"]
|
||||
+ attrs["new_manual_count"]
|
||||
== attrs["new_count"]
|
||||
)
|
||||
assert (
|
||||
attrs["changed_fail_count"]
|
||||
+ attrs["changed_pass_count"]
|
||||
+ attrs["changed_manual_count"]
|
||||
== attrs["changed_count"]
|
||||
)
|
||||
# And the *full* breakdown (including the muted halves) is exposed
|
||||
# so clients can also count muted-only deltas without losing data.
|
||||
assert new_total >= attrs["new_count"]
|
||||
assert changed_total >= attrs["changed_count"]
|
||||
|
||||
def test_finding_groups_resources_serializer_exposes_muted(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""The /finding-groups/<id>/resources payload must expose `muted`."""
|
||||
response = authenticated_client.get(
|
||||
reverse(
|
||||
"finding-group-resources",
|
||||
kwargs={"pk": "rds_encryption"},
|
||||
),
|
||||
{"filter[inserted_at]": TODAY},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data, "rds_encryption should expose its resources"
|
||||
for item in data:
|
||||
attrs = item["attributes"]
|
||||
assert "muted" in attrs and isinstance(attrs["muted"], bool)
|
||||
# rds_encryption has all muted findings
|
||||
assert attrs["muted"] is True
|
||||
# Status reflects the underlying check outcome (FAIL), not MUTED
|
||||
assert attrs["status"] == "FAIL"
|
||||
|
||||
def test_finding_groups_resources_exposes_finding_id(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""The /resources payload exposes the most recent matching finding_id.
|
||||
|
||||
rds_encryption has 2 findings, one per resource. Each resource row must
|
||||
report the UUID of its corresponding Finding (UUIDv7 ordering means
|
||||
Max(finding__id) resolves to the latest snapshot in time).
|
||||
"""
|
||||
response = authenticated_client.get(
|
||||
reverse(
|
||||
"finding-group-resources",
|
||||
kwargs={"pk": "rds_encryption"},
|
||||
),
|
||||
{"filter[inserted_at]": TODAY},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data, "rds_encryption should expose its resources"
|
||||
|
||||
rds_finding_ids = {
|
||||
str(f.id) for f in finding_groups_fixture if f.check_id == "rds_encryption"
|
||||
}
|
||||
assert rds_finding_ids, "fixture sanity"
|
||||
|
||||
for item in data:
|
||||
attrs = item["attributes"]
|
||||
assert "finding_id" in attrs
|
||||
assert attrs["finding_id"] in rds_finding_ids
|
||||
|
||||
def test_finding_groups_latest_resources_exposes_finding_id(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""The /latest/.../resources payload also exposes finding_id."""
|
||||
response = authenticated_client.get(
|
||||
reverse(
|
||||
"finding-group-latest_resources",
|
||||
kwargs={"check_id": "rds_encryption"},
|
||||
),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data, "rds_encryption should expose its resources via /latest"
|
||||
|
||||
rds_finding_ids = {
|
||||
str(f.id) for f in finding_groups_fixture if f.check_id == "rds_encryption"
|
||||
}
|
||||
for item in data:
|
||||
attrs = item["attributes"]
|
||||
assert "finding_id" in attrs
|
||||
assert attrs["finding_id"] in rds_finding_ids
|
||||
|
||||
@@ -39,6 +39,7 @@ 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):
|
||||
@@ -94,6 +95,7 @@ def return_prowler_provider(
|
||||
| MongodbatlasProvider
|
||||
| OpenstackProvider
|
||||
| OraclecloudProvider
|
||||
| VercelProvider
|
||||
):
|
||||
"""Return the Prowler provider class based on the given provider type.
|
||||
|
||||
@@ -175,6 +177,10 @@ 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
|
||||
@@ -235,6 +241,11 @@ 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").
|
||||
@@ -281,6 +292,7 @@ def initialize_prowler_provider(
|
||||
| MongodbatlasProvider
|
||||
| OpenstackProvider
|
||||
| OraclecloudProvider
|
||||
| VercelProvider
|
||||
):
|
||||
"""Initialize a Prowler provider instance based on the given provider type.
|
||||
|
||||
@@ -332,6 +344,13 @@ 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,
|
||||
@@ -415,8 +434,12 @@ 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,8 +69,10 @@ class SecurityHubConfigSerializer(BaseValidateSerializer):
|
||||
|
||||
class JiraConfigSerializer(BaseValidateSerializer):
|
||||
domain = serializers.CharField(read_only=True)
|
||||
issue_types = serializers.ListField(
|
||||
read_only=True, child=serializers.CharField(), default=["Task"]
|
||||
issue_types = serializers.DictField(
|
||||
read_only=True,
|
||||
child=serializers.ListField(child=serializers.CharField()),
|
||||
default={},
|
||||
)
|
||||
projects = serializers.DictField(read_only=True)
|
||||
|
||||
|
||||
@@ -404,6 +404,17 @@ 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,6 +1573,8 @@ 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}"}
|
||||
@@ -1779,6 +1781,13 @@ 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()
|
||||
@@ -2713,11 +2722,11 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
)
|
||||
config_serializer = JiraConfigSerializer
|
||||
# Create non-editable configuration for JIRA integration
|
||||
default_jira_issue_types = ["Task"]
|
||||
# issue_types will be populated per project when connection is tested
|
||||
configuration.update(
|
||||
{
|
||||
"projects": {},
|
||||
"issue_types": default_jira_issue_types,
|
||||
"issue_types": {},
|
||||
"domain": credentials.get("domain"),
|
||||
}
|
||||
)
|
||||
@@ -2932,13 +2941,25 @@ 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.ChoiceField(required=True, choices=["Task"])
|
||||
issue_type = serializers.CharField(required=True)
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "integrations-jira-dispatches"
|
||||
@@ -2967,6 +2988,23 @@ 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
|
||||
|
||||
|
||||
@@ -4147,6 +4185,7 @@ class FindingGroupSerializer(BaseSerializerV1):
|
||||
check_description = serializers.CharField(required=False, allow_null=True)
|
||||
severity = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
muted = serializers.BooleanField()
|
||||
impacted_providers = serializers.ListField(
|
||||
child=serializers.CharField(), required=False
|
||||
)
|
||||
@@ -4154,9 +4193,25 @@ class FindingGroupSerializer(BaseSerializerV1):
|
||||
resources_total = serializers.IntegerField()
|
||||
pass_count = serializers.IntegerField()
|
||||
fail_count = serializers.IntegerField()
|
||||
manual_count = serializers.IntegerField()
|
||||
pass_muted_count = serializers.IntegerField()
|
||||
fail_muted_count = serializers.IntegerField()
|
||||
manual_muted_count = serializers.IntegerField()
|
||||
muted_count = serializers.IntegerField()
|
||||
new_count = serializers.IntegerField()
|
||||
changed_count = serializers.IntegerField()
|
||||
new_fail_count = serializers.IntegerField()
|
||||
new_fail_muted_count = serializers.IntegerField()
|
||||
new_pass_count = serializers.IntegerField()
|
||||
new_pass_muted_count = serializers.IntegerField()
|
||||
new_manual_count = serializers.IntegerField()
|
||||
new_manual_muted_count = serializers.IntegerField()
|
||||
changed_fail_count = serializers.IntegerField()
|
||||
changed_fail_muted_count = serializers.IntegerField()
|
||||
changed_pass_count = serializers.IntegerField()
|
||||
changed_pass_muted_count = serializers.IntegerField()
|
||||
changed_manual_count = serializers.IntegerField()
|
||||
changed_manual_muted_count = serializers.IntegerField()
|
||||
first_seen_at = serializers.DateTimeField(required=False, allow_null=True)
|
||||
last_seen_at = serializers.DateTimeField(required=False, allow_null=True)
|
||||
failing_since = serializers.DateTimeField(required=False, allow_null=True)
|
||||
@@ -4170,14 +4225,18 @@ class FindingGroupResourceSerializer(BaseSerializerV1):
|
||||
Serializer for Finding Group Resources - resources within a finding group.
|
||||
|
||||
Returns individual resources with their current status, severity,
|
||||
and timing information.
|
||||
and timing information. Orphan findings (without any resource) expose the
|
||||
finding id as `id` so the row stays identifiable in the UI.
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="resource_id")
|
||||
id = serializers.UUIDField(source="row_id")
|
||||
resource = serializers.SerializerMethodField()
|
||||
provider = serializers.SerializerMethodField()
|
||||
finding_id = serializers.UUIDField()
|
||||
status = serializers.CharField()
|
||||
severity = serializers.CharField()
|
||||
muted = serializers.BooleanField()
|
||||
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)
|
||||
|
||||
+632
-129
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ from config.django.production import LOGGING as DJANGO_LOGGERS, DEBUG # noqa: E
|
||||
from config.custom_logging import BackendLogger # noqa: E402
|
||||
|
||||
BIND_ADDRESS = env("DJANGO_BIND_ADDRESS", default="127.0.0.1")
|
||||
PORT = env("DJANGO_PORT", default=8000)
|
||||
PORT = env("DJANGO_PORT", default=8080)
|
||||
|
||||
# Server settings
|
||||
bind = f"{BIND_ADDRESS}:{PORT}"
|
||||
|
||||
@@ -565,6 +565,12 @@ 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,
|
||||
@@ -579,6 +585,7 @@ def providers_fixture(tenants_fixture):
|
||||
provider10,
|
||||
provider11,
|
||||
provider12,
|
||||
provider13,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ This module handles:
|
||||
- Adding resource labels to Cartography nodes for efficient lookups
|
||||
- Loading Prowler findings into the graph
|
||||
- Linking findings to resources
|
||||
- Cleaning up stale findings
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -24,7 +23,6 @@ from tasks.jobs.attack_paths.config import (
|
||||
)
|
||||
from tasks.jobs.attack_paths.queries import (
|
||||
ADD_RESOURCE_LABEL_TEMPLATE,
|
||||
CLEANUP_FINDINGS_TEMPLATE,
|
||||
INSERT_FINDING_TEMPLATE,
|
||||
render_cypher_template,
|
||||
)
|
||||
@@ -92,14 +90,13 @@ def analysis(
|
||||
"""
|
||||
Main entry point for Prowler findings analysis.
|
||||
|
||||
Adds resource labels, loads findings, and cleans up stale data.
|
||||
Adds resource labels and loads findings.
|
||||
"""
|
||||
add_resource_label(
|
||||
neo4j_session, prowler_api_provider.provider, str(prowler_api_provider.uid)
|
||||
)
|
||||
findings_data = stream_findings_with_resources(prowler_api_provider, scan_id)
|
||||
load_findings(neo4j_session, findings_data, prowler_api_provider, config)
|
||||
cleanup_findings(neo4j_session, prowler_api_provider, config)
|
||||
|
||||
|
||||
def add_resource_label(
|
||||
@@ -183,28 +180,6 @@ def load_findings(
|
||||
logger.info(f"Finished loading {total_records} records in {batch_num} batches")
|
||||
|
||||
|
||||
def cleanup_findings(
|
||||
neo4j_session: neo4j.Session,
|
||||
prowler_api_provider: Provider,
|
||||
config: CartographyConfig,
|
||||
) -> None:
|
||||
"""Remove stale findings (classic Cartography behaviour)."""
|
||||
parameters = {
|
||||
"last_updated": config.update_tag,
|
||||
"batch_size": BATCH_SIZE,
|
||||
}
|
||||
|
||||
batch = 1
|
||||
deleted_count = 1
|
||||
while deleted_count > 0:
|
||||
logger.info(f"Cleaning findings batch {batch}")
|
||||
|
||||
result = neo4j_session.run(CLEANUP_FINDINGS_TEMPLATE, parameters)
|
||||
|
||||
deleted_count = result.single().get("deleted_findings_count", 0)
|
||||
batch += 1
|
||||
|
||||
|
||||
# Findings Streaming (Generator-based)
|
||||
# -------------------------------------
|
||||
|
||||
@@ -273,7 +248,9 @@ def _fetch_findings_batch(
|
||||
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
|
||||
# Use `all_objects` to get `Findings` even on soft-deleted `Providers`
|
||||
# But even the provider is already validated as active in this context
|
||||
qs = FindingModel.all_objects.filter(scan_id=scan_id).order_by("id")
|
||||
qs = FindingModel.all_objects.filter(
|
||||
tenant_id=tenant_id, scan_id=scan_id
|
||||
).order_by("id")
|
||||
|
||||
if after_id is not None:
|
||||
qs = qs.filter(id__gt=after_id)
|
||||
|
||||
@@ -13,14 +13,13 @@ from tasks.jobs.attack_paths.config import (
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
# Indexes for Prowler findings and resource lookups
|
||||
# Indexes for Prowler Findings and resource lookups
|
||||
FINDINGS_INDEX_STATEMENTS = [
|
||||
# Resource indexes for Prowler Finding lookups
|
||||
"CREATE INDEX aws_resource_arn IF NOT EXISTS FOR (n:_AWSResource) ON (n.arn);",
|
||||
"CREATE INDEX aws_resource_id IF NOT EXISTS FOR (n:_AWSResource) ON (n.id);",
|
||||
# Prowler Finding indexes
|
||||
f"CREATE INDEX prowler_finding_id IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.id);",
|
||||
f"CREATE INDEX prowler_finding_lastupdated IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.lastupdated);",
|
||||
f"CREATE INDEX prowler_finding_status IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.status);",
|
||||
# Internet node index for MERGE lookups
|
||||
f"CREATE INDEX internet_id IF NOT EXISTS FOR (n:{INTERNET_NODE_LABEL}) ON (n.id);",
|
||||
|
||||
@@ -80,17 +80,6 @@ INSERT_FINDING_TEMPLATE = f"""
|
||||
rel.lastupdated = $last_updated
|
||||
"""
|
||||
|
||||
CLEANUP_FINDINGS_TEMPLATE = f"""
|
||||
MATCH (finding:{PROWLER_FINDING_LABEL})
|
||||
WHERE finding.lastupdated < $last_updated
|
||||
|
||||
WITH finding LIMIT $batch_size
|
||||
|
||||
DETACH DELETE finding
|
||||
|
||||
RETURN COUNT(finding) AS deleted_findings_count
|
||||
"""
|
||||
|
||||
# Internet queries (used by internet.py)
|
||||
# ---------------------------------------
|
||||
|
||||
|
||||
@@ -32,9 +32,13 @@ from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS
|
||||
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.cis.cis_m365 import M365CIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
|
||||
from prowler.lib.outputs.compliance.cisa_scuba.cisa_scuba_googleworkspace import (
|
||||
GoogleWorkspaceCISASCuBA,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.csa.csa_alibabacloud import AlibabaCloudCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA
|
||||
@@ -93,7 +97,7 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name.startswith("iso27001_"), AWSISO27001),
|
||||
(lambda name: name.startswith("kisa"), AWSKISAISMSP),
|
||||
(lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS),
|
||||
(lambda name: name == "ccc_aws", CCC_AWS),
|
||||
(lambda name: name.startswith("ccc_"), CCC_AWS),
|
||||
(lambda name: name.startswith("c5_"), AWSC5),
|
||||
(lambda name: name.startswith("csa_"), AWSCSA),
|
||||
],
|
||||
@@ -102,7 +106,7 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name == "mitre_attack_azure", AzureMitreAttack),
|
||||
(lambda name: name.startswith("ens_"), AzureENS),
|
||||
(lambda name: name.startswith("iso27001_"), AzureISO27001),
|
||||
(lambda name: name == "ccc_azure", CCC_Azure),
|
||||
(lambda name: name.startswith("ccc_"), CCC_Azure),
|
||||
(lambda name: name == "prowler_threatscore_azure", ProwlerThreatScoreAzure),
|
||||
(lambda name: name == "c5_azure", AzureC5),
|
||||
(lambda name: name.startswith("csa_"), AzureCSA),
|
||||
@@ -113,7 +117,7 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name.startswith("ens_"), GCPENS),
|
||||
(lambda name: name.startswith("iso27001_"), GCPISO27001),
|
||||
(lambda name: name == "prowler_threatscore_gcp", ProwlerThreatScoreGCP),
|
||||
(lambda name: name == "ccc_gcp", CCC_GCP),
|
||||
(lambda name: name.startswith("ccc_"), CCC_GCP),
|
||||
(lambda name: name == "c5_gcp", GCPC5),
|
||||
(lambda name: name.startswith("csa_"), GCPCSA),
|
||||
],
|
||||
@@ -133,6 +137,10 @@ COMPLIANCE_CLASS_MAP = {
|
||||
"github": [
|
||||
(lambda name: name.startswith("cis_"), GithubCIS),
|
||||
],
|
||||
"googleworkspace": [
|
||||
(lambda name: name.startswith("cis_"), GoogleWorkspaceCIS),
|
||||
(lambda name: name.startswith("cisa_scuba_"), GoogleWorkspaceCISASCuBA),
|
||||
],
|
||||
"iac": [
|
||||
# IaC provider doesn't have specific compliance frameworks yet
|
||||
# Trivy handles its own compliance checks
|
||||
|
||||
@@ -1803,7 +1803,10 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
|
||||
# Aggregate findings by check_id for this scan
|
||||
# Aggregate findings by check_id for this scan.
|
||||
# `pass_count`, `fail_count` and `manual_count` only count non-muted
|
||||
# findings. Muted findings are tracked separately via the
|
||||
# `*_muted_count` fields.
|
||||
aggregated = (
|
||||
Finding.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
@@ -1814,9 +1817,50 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
|
||||
severity_order=Max(severity_case),
|
||||
pass_count=Count("id", filter=Q(status="PASS", muted=False)),
|
||||
fail_count=Count("id", filter=Q(status="FAIL", muted=False)),
|
||||
manual_count=Count("id", filter=Q(status="MANUAL", muted=False)),
|
||||
pass_muted_count=Count("id", filter=Q(status="PASS", muted=True)),
|
||||
fail_muted_count=Count("id", filter=Q(status="FAIL", muted=True)),
|
||||
manual_muted_count=Count("id", filter=Q(status="MANUAL", muted=True)),
|
||||
muted_count=Count("id", filter=Q(muted=True)),
|
||||
nonmuted_count=Count("id", filter=Q(muted=False)),
|
||||
new_count=Count("id", filter=Q(delta="new", muted=False)),
|
||||
changed_count=Count("id", filter=Q(delta="changed", muted=False)),
|
||||
new_fail_count=Count(
|
||||
"id", filter=Q(delta="new", status="FAIL", muted=False)
|
||||
),
|
||||
new_fail_muted_count=Count(
|
||||
"id", filter=Q(delta="new", status="FAIL", muted=True)
|
||||
),
|
||||
new_pass_count=Count(
|
||||
"id", filter=Q(delta="new", status="PASS", muted=False)
|
||||
),
|
||||
new_pass_muted_count=Count(
|
||||
"id", filter=Q(delta="new", status="PASS", muted=True)
|
||||
),
|
||||
new_manual_count=Count(
|
||||
"id", filter=Q(delta="new", status="MANUAL", muted=False)
|
||||
),
|
||||
new_manual_muted_count=Count(
|
||||
"id", filter=Q(delta="new", status="MANUAL", muted=True)
|
||||
),
|
||||
changed_fail_count=Count(
|
||||
"id", filter=Q(delta="changed", status="FAIL", muted=False)
|
||||
),
|
||||
changed_fail_muted_count=Count(
|
||||
"id", filter=Q(delta="changed", status="FAIL", muted=True)
|
||||
),
|
||||
changed_pass_count=Count(
|
||||
"id", filter=Q(delta="changed", status="PASS", muted=False)
|
||||
),
|
||||
changed_pass_muted_count=Count(
|
||||
"id", filter=Q(delta="changed", status="PASS", muted=True)
|
||||
),
|
||||
changed_manual_count=Count(
|
||||
"id", filter=Q(delta="changed", status="MANUAL", muted=False)
|
||||
),
|
||||
changed_manual_muted_count=Count(
|
||||
"id", filter=Q(delta="changed", status="MANUAL", muted=True)
|
||||
),
|
||||
resources_total=Count("resources__id", distinct=True),
|
||||
resources_fail=Count(
|
||||
"resources__id",
|
||||
@@ -1824,7 +1868,9 @@ 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"),
|
||||
agg_first_seen_at=Min(
|
||||
"first_seen_at", filter=Q(delta="new", muted=False)
|
||||
),
|
||||
agg_last_seen_at=Max("inserted_at"),
|
||||
agg_failing_since=Min(
|
||||
"first_seen_at", filter=Q(status="FAIL", muted=False)
|
||||
@@ -1893,9 +1939,26 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
|
||||
severity_order=row["severity_order"] or 1,
|
||||
pass_count=row["pass_count"],
|
||||
fail_count=row["fail_count"],
|
||||
manual_count=row["manual_count"],
|
||||
pass_muted_count=row["pass_muted_count"],
|
||||
fail_muted_count=row["fail_muted_count"],
|
||||
manual_muted_count=row["manual_muted_count"],
|
||||
muted_count=row["muted_count"],
|
||||
muted=row["nonmuted_count"] == 0,
|
||||
new_count=row["new_count"],
|
||||
changed_count=row["changed_count"],
|
||||
new_fail_count=row["new_fail_count"],
|
||||
new_fail_muted_count=row["new_fail_muted_count"],
|
||||
new_pass_count=row["new_pass_count"],
|
||||
new_pass_muted_count=row["new_pass_muted_count"],
|
||||
new_manual_count=row["new_manual_count"],
|
||||
new_manual_muted_count=row["new_manual_muted_count"],
|
||||
changed_fail_count=row["changed_fail_count"],
|
||||
changed_fail_muted_count=row["changed_fail_muted_count"],
|
||||
changed_pass_count=row["changed_pass_count"],
|
||||
changed_pass_muted_count=row["changed_pass_muted_count"],
|
||||
changed_manual_count=row["changed_manual_count"],
|
||||
changed_manual_muted_count=row["changed_manual_muted_count"],
|
||||
resources_total=row["resources_total"],
|
||||
resources_fail=row["resources_fail"],
|
||||
first_seen_at=row["agg_first_seen_at"],
|
||||
@@ -1915,9 +1978,26 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str):
|
||||
"severity_order",
|
||||
"pass_count",
|
||||
"fail_count",
|
||||
"manual_count",
|
||||
"pass_muted_count",
|
||||
"fail_muted_count",
|
||||
"manual_muted_count",
|
||||
"muted_count",
|
||||
"muted",
|
||||
"new_count",
|
||||
"changed_count",
|
||||
"new_fail_count",
|
||||
"new_fail_muted_count",
|
||||
"new_pass_count",
|
||||
"new_pass_muted_count",
|
||||
"new_manual_count",
|
||||
"new_manual_muted_count",
|
||||
"changed_fail_count",
|
||||
"changed_fail_muted_count",
|
||||
"changed_pass_count",
|
||||
"changed_pass_muted_count",
|
||||
"changed_manual_count",
|
||||
"changed_manual_muted_count",
|
||||
"resources_total",
|
||||
"resources_fail",
|
||||
"first_seen_at",
|
||||
|
||||
@@ -771,26 +771,49 @@ def aggregate_finding_group_summaries_task(tenant_id: str, scan_id: str):
|
||||
)
|
||||
@set_tenant(keep_tenant=True)
|
||||
def reaggregate_all_finding_group_summaries_task(tenant_id: str):
|
||||
"""Reaggregate finding group summaries for all providers' latest completed scans."""
|
||||
latest_scan_ids = list(
|
||||
Scan.objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
|
||||
.order_by("provider_id", "-completed_at", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
"""Reaggregate finding group summaries for every (provider, day) combination.
|
||||
|
||||
Mirrors the unbounded scope of `mute_historical_findings_task`: that task
|
||||
rewrites every Finding row whose UID matches a mute rule, with no time
|
||||
limit. To keep the daily summaries consistent with that update, this task
|
||||
re-runs the aggregator on the latest completed scan of every (provider,
|
||||
day) pair that exists in the database. Tasks are dispatched in parallel
|
||||
via a Celery group so the wallclock scales with the worker pool, not with
|
||||
the number of pairs.
|
||||
"""
|
||||
completed_scans = list(
|
||||
Scan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
state=StateChoices.COMPLETED,
|
||||
completed_at__isnull=False,
|
||||
)
|
||||
.order_by("-completed_at")
|
||||
.values("id", "completed_at", "provider_id")
|
||||
)
|
||||
if latest_scan_ids:
|
||||
|
||||
# Keep the latest scan per (provider, day) pair so the daily summary row
|
||||
# the aggregator writes is the most recent snapshot of that day for that
|
||||
# provider. Iterating from most recent to oldest means the first scan we
|
||||
# see for a given key wins.
|
||||
latest_scans: dict[tuple, str] = {}
|
||||
for scan in completed_scans:
|
||||
key = (scan["provider_id"], scan["completed_at"].date())
|
||||
if key not in latest_scans:
|
||||
latest_scans[key] = str(scan["id"])
|
||||
|
||||
scan_ids = list(latest_scans.values())
|
||||
if scan_ids:
|
||||
logger.info(
|
||||
"Reaggregating finding group summaries for %d scans: %s",
|
||||
len(latest_scan_ids),
|
||||
latest_scan_ids,
|
||||
"Reaggregating finding group summaries for %d scans (provider x day)",
|
||||
len(scan_ids),
|
||||
)
|
||||
group(
|
||||
aggregate_finding_group_summaries_task.si(
|
||||
tenant_id=tenant_id, scan_id=str(scan_id)
|
||||
tenant_id=tenant_id, scan_id=scan_id
|
||||
)
|
||||
for scan_id in latest_scan_ids
|
||||
for scan_id in scan_ids
|
||||
).apply_async()
|
||||
return {"scans_reaggregated": len(latest_scan_ids)}
|
||||
return {"scans_reaggregated": len(scan_ids)}
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="lighthouse-connection-check")
|
||||
|
||||
@@ -1298,23 +1298,6 @@ class TestAttackPathsFindingsHelpers:
|
||||
assert params["last_updated"] == config.update_tag
|
||||
assert "findings_data" in params
|
||||
|
||||
def test_cleanup_findings_runs_batches(self, providers_fixture):
|
||||
provider = providers_fixture[0]
|
||||
config = SimpleNamespace(update_tag=1024)
|
||||
mock_session = MagicMock()
|
||||
|
||||
first_batch = MagicMock()
|
||||
first_batch.single.return_value = {"deleted_findings_count": 3}
|
||||
second_batch = MagicMock()
|
||||
second_batch.single.return_value = {"deleted_findings_count": 0}
|
||||
mock_session.run.side_effect = [first_batch, second_batch]
|
||||
|
||||
findings_module.cleanup_findings(mock_session, provider, config)
|
||||
|
||||
assert mock_session.run.call_count == 2
|
||||
params = mock_session.run.call_args.args[1]
|
||||
assert params["last_updated"] == config.update_tag
|
||||
|
||||
def test_stream_findings_with_resources_returns_latest_scan_data(
|
||||
self,
|
||||
tenants_fixture,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import openai
|
||||
@@ -2362,35 +2362,96 @@ class TestReaggregateAllFindingGroupSummaries:
|
||||
@patch("tasks.tasks.group")
|
||||
@patch("tasks.tasks.aggregate_finding_group_summaries_task")
|
||||
@patch("tasks.tasks.Scan.objects.filter")
|
||||
def test_dispatches_subtasks_for_each_provider(
|
||||
def test_dispatches_subtasks_for_each_provider_per_day(
|
||||
self, mock_scan_filter, mock_agg_task, mock_group
|
||||
):
|
||||
scan_id_1 = uuid.uuid4()
|
||||
scan_id_2 = uuid.uuid4()
|
||||
provider_id_1 = uuid.uuid4()
|
||||
provider_id_2 = uuid.uuid4()
|
||||
scan_id_today_p1 = uuid.uuid4()
|
||||
scan_id_yesterday_p1 = uuid.uuid4()
|
||||
scan_id_today_p2 = uuid.uuid4()
|
||||
today = datetime.now(tz=timezone.utc)
|
||||
yesterday = today - timedelta(days=1)
|
||||
|
||||
mock_group_result = MagicMock()
|
||||
mock_group.side_effect = lambda gen: (list(gen), mock_group_result)[1]
|
||||
|
||||
mock_scan_filter.return_value.order_by.return_value.distinct.return_value.values_list.return_value = [
|
||||
scan_id_1,
|
||||
scan_id_2,
|
||||
mock_scan_filter.return_value.order_by.return_value.values.return_value = [
|
||||
{
|
||||
"id": scan_id_today_p1,
|
||||
"completed_at": today,
|
||||
"provider_id": provider_id_1,
|
||||
},
|
||||
{
|
||||
"id": scan_id_today_p2,
|
||||
"completed_at": today,
|
||||
"provider_id": provider_id_2,
|
||||
},
|
||||
{
|
||||
"id": scan_id_yesterday_p1,
|
||||
"completed_at": yesterday,
|
||||
"provider_id": provider_id_1,
|
||||
},
|
||||
]
|
||||
|
||||
result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id)
|
||||
|
||||
assert result == {"scans_reaggregated": 2}
|
||||
assert mock_agg_task.si.call_count == 2
|
||||
assert result == {"scans_reaggregated": 3}
|
||||
assert mock_agg_task.si.call_count == 3
|
||||
mock_agg_task.si.assert_any_call(
|
||||
tenant_id=self.tenant_id, scan_id=str(scan_id_1)
|
||||
tenant_id=self.tenant_id, scan_id=str(scan_id_today_p1)
|
||||
)
|
||||
mock_agg_task.si.assert_any_call(
|
||||
tenant_id=self.tenant_id, scan_id=str(scan_id_2)
|
||||
tenant_id=self.tenant_id, scan_id=str(scan_id_today_p2)
|
||||
)
|
||||
mock_agg_task.si.assert_any_call(
|
||||
tenant_id=self.tenant_id, scan_id=str(scan_id_yesterday_p1)
|
||||
)
|
||||
mock_group_result.apply_async.assert_called_once()
|
||||
|
||||
@patch("tasks.tasks.group")
|
||||
@patch("tasks.tasks.aggregate_finding_group_summaries_task")
|
||||
@patch("tasks.tasks.Scan.objects.filter")
|
||||
def test_dedupes_scans_to_latest_per_provider_per_day(
|
||||
self, mock_scan_filter, mock_agg_task, mock_group
|
||||
):
|
||||
"""When several scans run on the same day for the same provider, only
|
||||
the latest one is dispatched (matching the daily summary unique key)."""
|
||||
provider_id = uuid.uuid4()
|
||||
latest_scan_today = uuid.uuid4()
|
||||
earlier_scan_today = uuid.uuid4()
|
||||
today_late = datetime.now(tz=timezone.utc)
|
||||
today_early = today_late - timedelta(hours=4)
|
||||
|
||||
mock_group_result = MagicMock()
|
||||
mock_group.side_effect = lambda gen: (list(gen), mock_group_result)[1]
|
||||
|
||||
# Returned ordered by `-completed_at`, so the most recent comes first.
|
||||
mock_scan_filter.return_value.order_by.return_value.values.return_value = [
|
||||
{
|
||||
"id": latest_scan_today,
|
||||
"completed_at": today_late,
|
||||
"provider_id": provider_id,
|
||||
},
|
||||
{
|
||||
"id": earlier_scan_today,
|
||||
"completed_at": today_early,
|
||||
"provider_id": provider_id,
|
||||
},
|
||||
]
|
||||
|
||||
result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id)
|
||||
|
||||
assert result == {"scans_reaggregated": 1}
|
||||
mock_agg_task.si.assert_called_once_with(
|
||||
tenant_id=self.tenant_id, scan_id=str(latest_scan_today)
|
||||
)
|
||||
mock_group_result.apply_async.assert_called_once()
|
||||
|
||||
@patch("tasks.tasks.group")
|
||||
@patch("tasks.tasks.Scan.objects.filter")
|
||||
def test_no_completed_scans_skips_dispatch(self, mock_scan_filter, mock_group):
|
||||
mock_scan_filter.return_value.order_by.return_value.distinct.return_value.values_list.return_value = []
|
||||
mock_scan_filter.return_value.order_by.return_value.values.return_value = []
|
||||
|
||||
result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id)
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ spec:
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.worker.initContainers }}
|
||||
initContainers:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: worker
|
||||
{{- with .Values.worker.securityContext }}
|
||||
|
||||
@@ -32,6 +32,10 @@ spec:
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.worker_beat.initContainers }}
|
||||
initContainers:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: worker-beat
|
||||
{{- with .Values.worker_beat.securityContext }}
|
||||
|
||||
@@ -220,7 +220,7 @@ def _send_prowler_results(prowler_results, _prowler_version, options):
|
||||
try:
|
||||
_debug("RESULT MSG --- {0}".format(_check_result), 2)
|
||||
_check_result = json.loads(TEMPLATE_CHECK.format(_check_result))
|
||||
except:
|
||||
except Exception:
|
||||
_debug(
|
||||
"INVALID JSON --- {0}".format(TEMPLATE_CHECK.format(_check_result)), 1
|
||||
)
|
||||
|
||||
+17
-10
@@ -1,4 +1,11 @@
|
||||
services:
|
||||
api-dev-init:
|
||||
image: busybox:1.37.0
|
||||
volumes:
|
||||
- ./_data/api:/data
|
||||
command: ["sh", "-c", "chown -R 1000:1000 /data"]
|
||||
restart: "no"
|
||||
|
||||
api-dev:
|
||||
hostname: "prowler-api"
|
||||
image: prowler-api-dev
|
||||
@@ -21,12 +28,20 @@ services:
|
||||
- ./_data/api:/home/prowler/.config/prowler-api
|
||||
- outputs:/tmp/prowler_api_output
|
||||
depends_on:
|
||||
api-dev-init:
|
||||
condition: service_completed_successfully
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/api/v1/ || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 60s
|
||||
entrypoint:
|
||||
- "/home/prowler/docker-entrypoint.sh"
|
||||
- "dev"
|
||||
@@ -139,11 +154,7 @@ services:
|
||||
- ./api/docker-entrypoint.sh:/home/prowler/docker-entrypoint.sh
|
||||
- outputs:/tmp/prowler_api_output
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
api-dev:
|
||||
condition: service_healthy
|
||||
ulimits:
|
||||
nofile:
|
||||
@@ -165,11 +176,7 @@ services:
|
||||
- path: ./.env
|
||||
required: false
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
api-dev:
|
||||
condition: service_healthy
|
||||
ulimits:
|
||||
nofile:
|
||||
|
||||
+17
-6
@@ -5,6 +5,13 @@
|
||||
# docker compose -f docker-compose-dev.yml up
|
||||
#
|
||||
services:
|
||||
api-init:
|
||||
image: busybox:1.37.0
|
||||
volumes:
|
||||
- ./_data/api:/data
|
||||
command: ["sh", "-c", "chown -R 1000:1000 /data"]
|
||||
restart: "no"
|
||||
|
||||
api:
|
||||
hostname: "prowler-api"
|
||||
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
|
||||
@@ -17,12 +24,20 @@ services:
|
||||
- ./_data/api:/home/prowler/.config/prowler-api
|
||||
- output:/tmp/prowler_api_output
|
||||
depends_on:
|
||||
api-init:
|
||||
condition: service_completed_successfully
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/api/v1/ || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 60s
|
||||
entrypoint:
|
||||
- "/home/prowler/docker-entrypoint.sh"
|
||||
- "prod"
|
||||
@@ -114,9 +129,7 @@ services:
|
||||
volumes:
|
||||
- "output:/tmp/prowler_api_output"
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ulimits:
|
||||
nofile:
|
||||
@@ -132,9 +145,7 @@ services:
|
||||
- path: ./.env
|
||||
required: false
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ulimits:
|
||||
nofile:
|
||||
|
||||
@@ -118,18 +118,22 @@ In case you have any doubts, consult the [Poetry environment activation guide](h
|
||||
|
||||
### Pre-Commit Hooks
|
||||
|
||||
This repository uses Git pre-commit hooks managed by the [pre-commit](https://pre-commit.com/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository:
|
||||
This repository uses Git pre-commit hooks managed by the [prek](https://prek.j178.dev/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository:
|
||||
|
||||
```shell
|
||||
pre-commit install
|
||||
prek install
|
||||
```
|
||||
|
||||
Successful installation should produce the following output:
|
||||
|
||||
```shell
|
||||
pre-commit installed at .git/hooks/pre-commit
|
||||
prek installed at `.git/hooks/pre-commit`
|
||||
```
|
||||
|
||||
<Warning>
|
||||
If pre-commit hooks were previously installed, run `prek install --overwrite` to replace the existing hook. Otherwise, both tools will run on each commit.
|
||||
</Warning>
|
||||
|
||||
### Code Quality and Security Checks
|
||||
|
||||
Before merging pull requests, several automated checks and utilities ensure code security and updated dependencies:
|
||||
@@ -163,6 +167,8 @@ These resources help ensure that AI-assisted contributions maintain consistency
|
||||
|
||||
All dependencies are listed in the `pyproject.toml` file.
|
||||
|
||||
The SDK keeps direct dependencies pinned to exact versions, while `poetry.lock` records the full resolved dependency tree and the artifact hashes for every package. Use `poetry install` from the lock file instead of ad-hoc `pip` installs when you need a reproducible environment.
|
||||
|
||||
For proper code documentation, refer to the following and follow the code documentation practices presented there: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings).
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -15,8 +15,7 @@ This document describes the internal architecture of Prowler Lighthouse AI, enab
|
||||
|
||||
Lighthouse AI operates as a Langchain-based agent that connects Large Language Models (LLMs) with Prowler security data through the Model Context Protocol (MCP).
|
||||
|
||||
<img className="block dark:hidden" src="/images/lighthouse-architecture-light.png" alt="Prowler Lighthouse Architecture" />
|
||||
<img className="hidden dark:block" src="/images/lighthouse-architecture-dark.png" alt="Prowler Lighthouse Architecture" />
|
||||

|
||||
|
||||
### Three-Tier Architecture
|
||||
|
||||
|
||||
@@ -750,6 +750,35 @@ 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:**
|
||||
|
||||
+23
-1
@@ -12,6 +12,24 @@
|
||||
"dark": "/images/prowler-logo-white.png",
|
||||
"light": "/images/prowler-logo-black.png"
|
||||
},
|
||||
"contextual": {
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
{
|
||||
"title": "Request a feature",
|
||||
"description": "Open a feature request on GitHub",
|
||||
"icon": "plus",
|
||||
"href": "https://github.com/prowler-cloud/prowler/issues/new?template=feature-request.yml"
|
||||
},
|
||||
{
|
||||
"title": "Report an issue",
|
||||
"description": "Open a bug report on GitHub",
|
||||
"icon": "bug",
|
||||
"href": "https://github.com/prowler-cloud/prowler/issues/new?template=bug_report.yml"
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"tabs": [
|
||||
{
|
||||
@@ -98,6 +116,7 @@
|
||||
]
|
||||
},
|
||||
"user-guide/tutorials/prowler-app-rbac",
|
||||
"user-guide/tutorials/prowler-app-multi-tenant",
|
||||
"user-guide/tutorials/prowler-app-api-keys",
|
||||
"user-guide/tutorials/prowler-app-import-findings",
|
||||
{
|
||||
@@ -132,11 +151,13 @@
|
||||
]
|
||||
},
|
||||
"user-guide/tutorials/prowler-app-attack-paths",
|
||||
"user-guide/tutorials/prowler-app-finding-groups",
|
||||
"user-guide/tutorials/prowler-cloud-public-ips",
|
||||
{
|
||||
"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"
|
||||
]
|
||||
@@ -274,7 +295,8 @@
|
||||
{
|
||||
"group": "Image",
|
||||
"pages": [
|
||||
"user-guide/providers/image/getting-started-image"
|
||||
"user-guide/providers/image/getting-started-image",
|
||||
"user-guide/providers/image/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 | 27 tools | Yes |
|
||||
| Prowler Cloud/App | 29 tools | Yes |
|
||||
|
||||
## Tool Naming Convention
|
||||
|
||||
@@ -60,6 +60,7 @@ 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
|
||||
@@ -87,6 +88,7 @@ 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
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.22.0"
|
||||
PROWLER_API_VERSION="5.22.0"
|
||||
PROWLER_UI_VERSION="5.24.0"
|
||||
PROWLER_API_VERSION="5.24.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 192 KiB |
@@ -11,16 +11,19 @@ Prowler App is a web application that simplifies running Prowler. It provides:
|
||||
|
||||
## Components
|
||||
|
||||
Prowler App consists of three main components:
|
||||
Prowler App consists of four 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)
|
||||
|
||||

|
||||
|
||||
@@ -59,6 +59,10 @@ Prowler Lighthouse AI is powerful, but there are limitations:
|
||||
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
|
||||
- **Response quality**: The response quality depends on the selected LLM provider and model. Choose models with strong tool-calling capabilities for best results. We recommend `gpt-5` model from OpenAI.
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
## Extending Lighthouse AI
|
||||
|
||||
Lighthouse AI retrieves data through Prowler MCP. To add new capabilities, extend the Prowler MCP Server with additional tools and Lighthouse AI discovers them automatically.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user