Compare commits

..

71 Commits

Author SHA1 Message Date
Daniel Barranquero 6df8261a7e feat: add changelog 2026-03-23 10:43:26 +01:00
Daniel Barranquero b447f64ab0 feat(s3): add new check s3_bucket_website_hosting_disabled 2026-03-23 09:34:31 +01:00
Adrián Peña ad6368a446 chore: add defusedxml as api dependency (#10401) 2026-03-19 18:26:55 +01:00
Adrián Peña 3361393b7d chore: update changelog (#10400) 2026-03-19 17:55:18 +01:00
Sandiyo Christan 0b7a21a70c fix(api): [security] use defusedxml to prevent XML bomb DoS in SAML metadata parsing (#10165)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Adrián Peña <adrianjpr@gmail.com>
2026-03-19 17:44:52 +01:00
Josema Camacho 872e6e239c perf(api): replace JOINs with pre-check in threat score aggregation query (#10394) 2026-03-19 17:30:06 +01:00
Adrián Peña 2fe92cfce3 feat(api): add check title search for finding groups (#10377) 2026-03-19 16:48:26 +01:00
César Arroba cece2cb87e chore: pin Prowler version to lastest master commit on push (#10384) 2026-03-19 14:32:38 +01:00
Adrián Peña ab266080d0 perf(api): add trigram indexes for finding groups (#10378) 2026-03-19 13:54:50 +01:00
Prowler Bot 4638b39ed4 chore(api): Bump version to v1.23.0 (#10393)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:42:46 +01:00
Prowler Bot 997f9bf64a docs: Update version to v5.21.0 (#10391)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:40:33 +01:00
Prowler Bot aecc234f78 chore(release): Bump version to v5.22.0 (#10389)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:40:22 +01:00
Pepe Fagoaga 8317eff67b chore(changelog): prepare for v5.21.0 (#10380) 2026-03-19 11:09:51 +01:00
Rubén De la Torre Vico 5c4ee0bc48 chore(mcp): bump MCP server version to 0.5.0 (#10383) 2026-03-19 10:47:46 +01:00
rchotacode 0f2fdcfb3f chore(oraclecloud): Add Oracle Defense Cloud Support (#10376)
Co-authored-by: Ronan Chota <ronan.chota@saic.com>
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-03-19 09:41:58 +00:00
Rubén De la Torre Vico 11a8873155 feat(ui): add attack path custom query skill for Lighthouse AI (#10323)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-03-18 19:35:50 +01:00
Pedro Martín 5a3475bed3 feat(compliance): add SecNumCloud 3.2 for Oracle Cloud (#10371) 2026-03-18 12:28:38 +01:00
Pedro Martín bc43eed736 feat(compliance): add SecNumCloud 3.2 for AlibabaCloud (#10370) 2026-03-18 10:40:58 +01:00
Rubén De la Torre Vico 8c1e69b542 feat(mcp): add cartography schema tool for attack paths (#10321) 2026-03-18 10:39:04 +01:00
Rubén De la Torre Vico 75c4f11475 feat(ui): add skills system infrastructure to Lighthouse AI (#10322)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-03-18 10:28:46 +01:00
Josema Camacho 1da10611e7 perf(attack-paths): reduce sync and findings memory usage with smaller batches and cursor iteration (#10359) 2026-03-18 10:08:30 +01:00
Andoni Alonso e8aaf5266a chore(sdk): bump pygithub from 2.5.0 to 2.8.0 (#10353) 2026-03-18 09:58:40 +01:00
Josema Camacho f5f1f1ab2d fix(attack-paths): recover graph_data_ready when scan fails during graph swap (#10354) 2026-03-18 09:49:45 +01:00
Andoni Alonso 65e745d779 fix(sdk): skip strict CheckMetadata validators for external tool providers (#10363) 2026-03-18 09:11:39 +01:00
Pedro Martín 907664093f feat(compliance): add SecNumCloud 3.2 for GCP (#10364) 2026-03-18 08:38:06 +01:00
Pedro Martín 8c2e2332d7 feat(compliance): add SecNumCloud 3.2 for Azure (#10358) 2026-03-18 08:28:40 +01:00
tejas_0007 cb03573599 feat(compliance): Add RBI Cyber Security Framework for GCP (#10339)
Co-authored-by: Tejas Saubhage <tsaubhage0007@gmail.com>
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2026-03-17 15:55:30 +01:00
Pedro Martín b7571abaeb fix(dashboard): add RBI compliance dashboard support for Azure (#10360) 2026-03-17 15:42:39 +01:00
lydiavilchez 4f93a89d1b feat(ui): add Google Workspace provider integration (#10333)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-03-17 13:28:28 +01:00
Sandiyo Christan 88ce188103 fix(api): [security] use psycopg2.sql to safely compose DDL in PostgresEnumMigration (#10166)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Adrián Peña <adrianjpr@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-17 13:24:24 +01:00
Pawan Gambhir df680ef277 fix(route53): resolve false positive in dangling IP check (#9952)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-03-17 12:02:48 +01:00
Andoni Alonso 451071d694 feat(image): add image provider to UI (#10167)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2026-03-17 10:53:37 +01:00
Zakir Jiwani 887a20f06e feat: CORS_ALLOWED_ORIGINS configurable via environment variable (#10355)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-17 09:55:06 +01:00
Pedro Martín 712da2cf98 feat(ui): Add CloudTrail Events tab to detail cards (#10320)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-03-17 09:45:29 +01:00
Josema Camacho 6a4278ed4d fix(docs): setting a couple of API PRs in the next release instead of 5.20 (#10357) 2026-03-17 09:00:56 +01:00
Pepe Fagoaga febd2c8fdb fix(ci): checkout upstream repo for tests (#10356) 2026-03-17 08:47:12 +01:00
Josema Camacho 787a339cd9 feat(attack-paths): scans add tenant and provider related labels to nodes (#10308) 2026-03-16 16:31:15 +01:00
shria :)) 1cf6eaa0b7 feat(github): add organization_repository_deletion_limited check (#10185)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2026-03-16 16:22:36 +01:00
Josema Camacho b311456160 fix(security): Ignore cryptography vulnerability until we can upgrade it (#10345) 2026-03-16 13:19:37 +01:00
Josema Camacho ad02801c74 refactor(attack-paths): complete migration to private graph labels and properties (phase 2) (#10268) 2026-03-16 12:34:58 +01:00
Daniel Barranquero 361f8548bf feat(azure): add 'entra_conditional_access_policy_require_mfa_for_admin_portals' check and update compliance (#10330) 2026-03-16 12:14:58 +01:00
Prowler Bot 2b7b2623c5 feat(aws): Update regions for AWS services (#10341)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-16 12:02:57 +01:00
Pepe Fagoaga e9860f7002 chore: zizmor in pre-commit (#10343) 2026-03-16 11:32:06 +01:00
Alejandro Bailo b509fdf562 chore(ui): add changelog entry for org dropdown actions (#10317) (#10342) 2026-03-16 11:03:39 +01:00
Pedro Martín e197ad6fb0 chore(gitignore): add .claude (#10340) 2026-03-16 10:48:15 +01:00
Hugo Pereira Brito c9284f8003 chore(models): add pydantic validators for CheckMetadata (#8583)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2026-03-16 10:36:08 +01:00
Alejandro Bailo 4cd3b09818 feat(ui): add organization-specific actions to providers table dropdown (#10317) 2026-03-16 10:32:12 +01:00
Alejandro Bailo 22f79edec5 refactor(ui): replace HeroUI Snippet with CodeSnippet component (#10319) 2026-03-13 16:31:39 +01:00
dependabot[bot] 0790619020 chore(deps-dev): bump multipart from 1.3.0 to 1.3.1 (#10331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-13 12:36:16 +01:00
Daniel Barranquero 9df06095eb chore(azure): update ResourceType and Categories for entra metadata (#10334) 2026-03-13 12:13:47 +01:00
Pedro Martín 3672d19c6a feat(mutelisting): add mute button inside finding detailed view (#10303) 2026-03-13 11:45:10 +01:00
Rubén De la Torre Vico ebc792e578 chore(m365): enhance metadata for entra service (#9682)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-03-13 11:35:41 +01:00
Hugo Pereira Brito 534ad3d04f feat(m365): add entra_device_code_flow_blocked security check (#10218) 2026-03-13 11:31:47 +01:00
Rubén De la Torre Vico 37d59b118f chore(m365): enhance metadata for teams service (#9685)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-03-13 09:53:00 +01:00
dependabot[bot] 06e32e69c0 build(deps): bump actions/setup-node from 6.1.0 to 6.2.0 (#9933)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:51:27 +01:00
dependabot[bot] 6e9f54d1ba build(deps): bump peter-evans/create-pull-request from 8.0.0 to 8.1.0 (#9937)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:51:07 +01:00
Rubén De la Torre Vico b29cd7f6c7 chore(m365): enhance metadata for exchange service (#9683)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-03-13 09:47:56 +01:00
dependabot[bot] 41a7b19c7d build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#9936)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:46:40 +01:00
dependabot[bot] c972f19059 build(deps): bump actions/cache from 5.0.1 to 5.0.3 (#9934)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:45:26 +01:00
dependabot[bot] 27d074abe4 build(deps): bump actions/setup-python from 5.3.0 to 6.2.0 (#9932)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:44:28 +01:00
dependabot[bot] 28060064de build(deps): bump docker/login-action from 3.6.0 to 3.7.0 (#9931)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:41:34 +01:00
dependabot[bot] fd695b6992 build(deps): bump regclient/actions from f61d18f46c86af724a9c804cb9ff2a6fec741c7c to da9319db8e44e8b062b3a147e1dfb2f574d41a03 (#10202)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:41:16 +01:00
Rubén De la Torre Vico 2fff8cb416 chore(m365): enhance metadata for sharepoint service (#9684)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-03-13 09:40:49 +01:00
dependabot[bot] f55e87d659 build(deps): bump tj-actions/changed-files from 47.0.1 to 47.0.4 (#10203)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:40:31 +01:00
dependabot[bot] 29b835360a build(deps): bump aws-actions/configure-aws-credentials from 5.1.1 to 6.0.0 (#10205)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:40:18 +01:00
dependabot[bot] 16e15a3a71 build(deps): bump github/codeql-action from 4.31.9 to 4.32.4 (#10204)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:39:57 +01:00
dependabot[bot] a6d47bdb2b build(deps): bump docker/build-push-action from 6.18.0 to 6.19.2 (#10201)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 09:39:43 +01:00
Prowler Bot 712af7b9c9 chore(release): Bump version to v5.21.0 (#10328)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-03-13 08:55:03 +01:00
Pepe Fagoaga b8c6f3ba67 chore(skills): add Django migrations skills (#10260) 2026-03-12 18:37:43 +01:00
Prowler Bot 80a814afce chore(api): Bump version to v1.22.0 (#10326)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-12 18:26:23 +01:00
Prowler Bot 52facad35c docs: Update version to v5.20.0 (#10324)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-12 18:25:31 +01:00
381 changed files with 18730 additions and 2458 deletions
+10 -2
View File
@@ -26,10 +26,18 @@ runs:
if: github.event_name == 'pull_request' && github.base_ref == 'master' && github.repository == 'prowler-cloud/prowler'
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
echo "Using branch: $BRANCH_NAME"
sed -i "s|\(git+https://github.com/prowler-cloud/prowler[^@]*\)@master|\1@$BRANCH_NAME|g" pyproject.toml
UPSTREAM="prowler-cloud/prowler"
if [ "$HEAD_REPO" != "$UPSTREAM" ]; then
echo "Fork PR detected (${HEAD_REPO}), rewriting VCS URL to fork"
sed -i "s|git+https://github.com/prowler-cloud/prowler\([^@]*\)@master|git+https://github.com/${HEAD_REPO}\1@$BRANCH_NAME|g" pyproject.toml
else
echo "Same-repo PR, using branch: $BRANCH_NAME"
sed -i "s|\(git+https://github.com/prowler-cloud/prowler[^@]*\)@master|\1@$BRANCH_NAME|g" pyproject.toml
fi
- name: Install poetry
shell: bash
+7 -7
View File
@@ -28,7 +28,7 @@ jobs:
current_api_version: ${{ steps.get_api_version.outputs.current_api_version }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -80,7 +80,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -118,7 +118,7 @@ jobs:
git --no-pager diff
- name: Create PR for next API minor version to master
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -137,7 +137,7 @@ jobs:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
- name: Checkout version branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }}
persist-credentials: false
@@ -177,7 +177,7 @@ jobs:
git --no-pager diff
- name: Create PR for first API patch version to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -205,7 +205,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -255,7 +255,7 @@ jobs:
git --no-pager diff
- name: Create PR for next API patch version to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
+2 -2
View File
@@ -33,14 +33,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for API changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
api/**
+3 -3
View File
@@ -42,17 +42,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/api-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: '/language:${{ matrix.language }}'
+13 -7
View File
@@ -57,7 +57,7 @@ jobs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -95,12 +95,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Pin prowler SDK to latest master commit
if: github.event_name == 'push'
run: |
LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1)
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@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -111,7 +117,7 @@ jobs:
- 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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.WORKING_DIRECTORY }}
push: true
@@ -129,7 +135,7 @@ jobs:
steps:
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -161,7 +167,7 @@ jobs:
- name: Install regctl
if: always()
uses: regclient/actions/regctl-installer@f61d18f46c86af724a9c804cb9ff2a6fec741c7c # main
uses: regclient/actions/regctl-installer@da9319db8e44e8b062b3a147e1dfb2f574d41a03 # main
- name: Cleanup intermediate architecture tags
if: always()
@@ -180,7 +186,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+5 -5
View File
@@ -28,14 +28,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check if Dockerfile changed
id: dockerfile-changed
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: api/Dockerfile
@@ -66,14 +66,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for API changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: api/**
files_ignore: |
@@ -88,7 +88,7 @@ jobs:
- name: Build container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.API_WORKING_DIR }}
push: false
+4 -3
View File
@@ -33,14 +33,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for API changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
api/**
@@ -64,8 +64,9 @@ jobs:
- name: Safety
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry run safety check --ignore 79023,79027
run: poetry run safety check --ignore 79023,79027,86217
# 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`
- name: Vulture
if: steps.check-changes.outputs.any_changed == 'true'
+2 -2
View File
@@ -73,14 +73,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for API changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
api/**
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+7 -7
View File
@@ -28,7 +28,7 @@ jobs:
current_docs_version: ${{ steps.get_docs_version.outputs.current_docs_version }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -80,7 +80,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -114,7 +114,7 @@ jobs:
git --no-pager diff
- name: Create PR for documentation update to master
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -137,7 +137,7 @@ jobs:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
- name: Checkout version branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }}
persist-credentials: false
@@ -174,7 +174,7 @@ jobs:
git --no-pager diff
- name: Create PR for documentation update to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -205,7 +205,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -245,7 +245,7 @@ jobs:
git --no-pager diff
- name: Create PR for documentation update to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
@@ -56,7 +56,7 @@ jobs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -93,12 +93,12 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -109,7 +109,7 @@ jobs:
- 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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.WORKING_DIRECTORY }}
push: true
@@ -135,7 +135,7 @@ jobs:
steps:
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -186,7 +186,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+5 -5
View File
@@ -28,14 +28,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check if Dockerfile changed
id: dockerfile-changed
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: mcp_server/Dockerfile
@@ -65,14 +65,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for MCP changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: mcp_server/**
files_ignore: |
@@ -85,7 +85,7 @@ jobs:
- name: Build MCP container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.MCP_WORKING_DIR }}
push: false
+2 -2
View File
@@ -60,7 +60,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -70,7 +70,7 @@ jobs:
enable-cache: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# zizmor: ignore[artipacked]
@@ -37,7 +37,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
api/**
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout PR head
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
@@ -34,7 +34,7 @@ jobs:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: '**'
+3 -3
View File
@@ -27,14 +27,14 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.12'
@@ -345,7 +345,7 @@ jobs:
- name: Create PR for API dependency update
if: ${{ env.PATCH_VERSION == '0' }}
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
commit-message: 'chore(api): update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}'
+6 -6
View File
@@ -67,7 +67,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -96,7 +96,7 @@ jobs:
git --no-pager diff
- name: Create PR for next minor version to master
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -115,7 +115,7 @@ jobs:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
- name: Checkout version branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }}
persist-credentials: false
@@ -148,7 +148,7 @@ jobs:
git --no-pager diff
- name: Create PR for first patch version to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -176,7 +176,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -211,7 +211,7 @@ jobs:
git --no-pager diff
- name: Create PR for next patch version to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+3 -3
View File
@@ -31,14 +31,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for SDK changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: ./**
files_ignore: |
@@ -67,7 +67,7 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
+3 -3
View File
@@ -49,17 +49,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/sdk-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: '/language:${{ matrix.language }}'
+10 -10
View File
@@ -61,12 +61,12 @@ jobs:
stable_tag: ${{ steps.get-prowler-version.outputs.stable_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -117,7 +117,7 @@ jobs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -155,18 +155,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Public ECR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: public.ecr.aws
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
@@ -180,7 +180,7 @@ jobs:
- 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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ${{ env.DOCKERFILE_PATH }}
@@ -199,13 +199,13 @@ jobs:
steps:
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Public ECR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: public.ecr.aws
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
@@ -266,7 +266,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+5 -5
View File
@@ -27,14 +27,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check if Dockerfile changed
id: dockerfile-changed
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: Dockerfile
@@ -65,14 +65,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for SDK changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: ./**
files_ignore: |
@@ -101,7 +101,7 @@ jobs:
- name: Build SDK container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: false
+4 -4
View File
@@ -59,7 +59,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -67,7 +67,7 @@ jobs:
run: pipx install poetry==2.1.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -100,7 +100,7 @@ jobs:
run: pipx install poetry==2.1.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -25,13 +25,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: 'master'
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -40,7 +40,7 @@ jobs:
run: pip install boto3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
@@ -51,7 +51,7 @@ jobs:
- name: Create pull request
id: create-pr
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>'
@@ -23,13 +23,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: 'master'
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -48,7 +48,7 @@ jobs:
- name: Create pull request
id: create-pr
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>'
@@ -72,12 +72,13 @@ jobs:
This PR updates the `OCI_COMMERCIAL_REGIONS` dictionary in `prowler/providers/oraclecloud/config.py` with the latest regions fetched from the OCI Identity API (`list_regions()`).
- Government regions (`OCI_GOVERNMENT_REGIONS`) are preserved unchanged
- DOD regions (`OCI_US_DOD_REGIONS`) are preserved unchanged
- Region display names are mapped from Oracle's official documentation
### Checklist
- [x] This is an automated update from OCI official sources
- [x] Government regions (us-langley-1, us-luke-1) preserved
- [x] Government regions (us-langley-1, us-luke-1) and DOD regions (us-gov-ashburn-1, us-gov-phoenix-1, us-gov-chicago-1) are preserved
- [x] No manual review of region data required
### License
+3 -3
View File
@@ -24,14 +24,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for SDK changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files:
./**
@@ -62,7 +62,7 @@ jobs:
- name: Set up Python 3.12
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.12'
cache: 'poetry'
+17 -17
View File
@@ -31,14 +31,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for SDK changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: ./**
files_ignore: |
@@ -67,7 +67,7 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
@@ -80,7 +80,7 @@ jobs:
- name: Check if AWS files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-aws
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/aws/**
@@ -210,7 +210,7 @@ jobs:
- name: Check if Azure files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-azure
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/azure/**
@@ -234,7 +234,7 @@ jobs:
- name: Check if GCP files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-gcp
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/gcp/**
@@ -258,7 +258,7 @@ jobs:
- name: Check if Kubernetes files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-kubernetes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/kubernetes/**
@@ -282,7 +282,7 @@ jobs:
- name: Check if GitHub files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-github
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/github/**
@@ -306,7 +306,7 @@ jobs:
- name: Check if NHN files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-nhn
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/nhn/**
@@ -330,7 +330,7 @@ jobs:
- name: Check if M365 files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-m365
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/m365/**
@@ -354,7 +354,7 @@ jobs:
- name: Check if IaC files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-iac
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/iac/**
@@ -378,7 +378,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@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/mongodbatlas/**
@@ -402,7 +402,7 @@ jobs:
- name: Check if OCI files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-oraclecloud
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/oraclecloud/**
@@ -426,7 +426,7 @@ jobs:
- name: Check if OpenStack files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-openstack
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/openstack/**
@@ -450,7 +450,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@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/**/googleworkspace/**
@@ -474,7 +474,7 @@ jobs:
- name: Check if Lib files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-lib
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/lib/**
@@ -498,7 +498,7 @@ jobs:
- name: Check if Config files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-config
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
./prowler/config/**
+3 -3
View File
@@ -48,17 +48,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
- name: Setup Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.12'
+6 -6
View File
@@ -67,7 +67,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -95,7 +95,7 @@ jobs:
git --no-pager diff
- name: Create PR for next minor version to master
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -117,7 +117,7 @@ jobs:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
- name: Checkout version branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }}
persist-credentials: false
@@ -149,7 +149,7 @@ jobs:
git --no-pager diff
- name: Create PR for first patch version to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -180,7 +180,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -214,7 +214,7 @@ jobs:
git --no-pager diff
- name: Create PR for next patch version to version branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
+3 -3
View File
@@ -45,17 +45,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/ui-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: '/language:${{ matrix.language }}'
@@ -59,7 +59,7 @@ jobs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -97,12 +97,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -113,7 +113,7 @@ jobs:
- 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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -185,7 +185,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+5 -5
View File
@@ -28,14 +28,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check if Dockerfile changed
id: dockerfile-changed
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: ui/Dockerfile
@@ -66,14 +66,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for UI changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: ui/**
files_ignore: |
@@ -87,7 +87,7 @@ jobs:
- name: Build UI container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.UI_WORKING_DIR }}
target: prod
+4 -4
View File
@@ -78,7 +78,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -152,7 +152,7 @@ jobs:
'
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '24.13.0'
@@ -166,7 +166,7 @@ jobs:
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm and Next.js cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.STORE_PATH }}
@@ -186,7 +186,7 @@ jobs:
run: pnpm run build
- name: Cache Playwright browsers
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
id: playwright-cache
with:
path: ~/.cache/ms-playwright
+6 -6
View File
@@ -30,14 +30,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for UI changes
id: check-changes
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
ui/**
@@ -50,7 +50,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@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
ui/**/*.ts
@@ -66,7 +66,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@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with:
files: |
ui/lib/**
@@ -78,7 +78,7 @@ jobs:
- name: Setup Node.js ${{ env.NODE_VERSION }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ env.NODE_VERSION }}
@@ -96,7 +96,7 @@ jobs:
- name: Setup pnpm and Next.js cache
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
${{ env.STORE_PATH }}
+3
View File
@@ -163,3 +163,6 @@ GEMINI.md
.codex/skills
.github/skills
.gemini/skills
# Claude Code
.claude/*
+9 -1
View File
@@ -22,6 +22,13 @@ repos:
args: [--autofix]
files: pyproject.toml
## GITHUB ACTIONS
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.6.0
hooks:
- id: zizmor
files: ^\.github/
## BASH
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
@@ -120,7 +127,8 @@ repos:
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
# 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
entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027'
# 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'
language: system
- id: vulture
+4 -2
View File
@@ -46,6 +46,8 @@ Use these skills for detailed patterns on-demand:
| `prowler-commit` | Professional commits (conventional-commits) | [SKILL.md](skills/prowler-commit/SKILL.md) |
| `prowler-pr` | Pull request conventions | [SKILL.md](skills/prowler-pr/SKILL.md) |
| `prowler-docs` | Documentation style guide | [SKILL.md](skills/prowler-docs/SKILL.md) |
| `django-migration-psql` | Django migration best practices for PostgreSQL | [SKILL.md](skills/django-migration-psql/SKILL.md) |
| `postgresql-indexing` | PostgreSQL indexing, EXPLAIN, monitoring, maintenance | [SKILL.md](skills/postgresql-indexing/SKILL.md) |
| `prowler-attack-paths-query` | Create Attack Paths openCypher queries | [SKILL.md](skills/prowler-attack-paths-query/SKILL.md) |
| `gh-aw` | GitHub Agentic Workflows (gh-aw) | [SKILL.md](skills/gh-aw/SKILL.md) |
| `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) |
@@ -85,15 +87,15 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Fixing bug | `tdd` |
| General Prowler development questions | `prowler` |
| Implementing JSON:API endpoints | `django-drf` |
| Importing Copilot Custom Agents into workflows | `gh-aw` |
| Implementing feature | `tdd` |
| Importing Copilot Custom Agents into workflows | `gh-aw` |
| Inspect PR CI checks and gates (.github/workflows/*) | `prowler-ci` |
| Inspect PR CI workflows (.github/workflows/*): conventional-commit, pr-check-changelog, pr-conflict-checker, labeler | `prowler-pr` |
| Mapping checks to compliance controls | `prowler-compliance` |
| Mocking AWS with moto in tests | `prowler-test-sdk` |
| Modifying API responses | `jsonapi` |
| Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` |
| Modifying component | `tdd` |
| Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` |
| Refactoring code | `tdd` |
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
+10
View File
@@ -4,6 +4,8 @@
> - [`prowler-api`](../skills/prowler-api/SKILL.md) - Models, Serializers, Views, RLS patterns
> - [`prowler-test-api`](../skills/prowler-test-api/SKILL.md) - Testing patterns (pytest-django)
> - [`prowler-attack-paths-query`](../skills/prowler-attack-paths-query/SKILL.md) - Attack Paths openCypher queries
> - [`django-migration-psql`](../skills/django-migration-psql/SKILL.md) - Migration best practices for PostgreSQL
> - [`postgresql-indexing`](../skills/postgresql-indexing/SKILL.md) - PostgreSQL indexing, EXPLAIN, monitoring, maintenance
> - [`django-drf`](../skills/django-drf/SKILL.md) - Generic DRF patterns
> - [`jsonapi`](../skills/jsonapi/SKILL.md) - Strict JSON:API v1.1 spec compliance
> - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns
@@ -16,14 +18,20 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|--------|-------|
| Add changelog entry for a PR or feature | `prowler-changelog` |
| Adding DRF pagination or permissions | `django-drf` |
| Adding indexes or constraints to database tables | `django-migration-psql` |
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
| Analyzing query performance with EXPLAIN | `postgresql-indexing` |
| Committing changes | `prowler-commit` |
| Create PR that requires changelog entry | `prowler-changelog` |
| Creating API endpoints | `jsonapi` |
| Creating Attack Paths queries | `prowler-attack-paths-query` |
| Creating ViewSets, serializers, or filters in api/ | `django-drf` |
| Creating a git commit | `prowler-commit` |
| Creating or modifying PostgreSQL indexes | `postgresql-indexing` |
| Creating or reviewing Django migrations | `django-migration-psql` |
| Creating/modifying models, views, serializers | `prowler-api` |
| Debugging slow queries or missing indexes | `postgresql-indexing` |
| Dropping or reindexing PostgreSQL indexes | `postgresql-indexing` |
| Fixing bug | `tdd` |
| Implementing JSON:API endpoints | `django-drf` |
| Implementing feature | `tdd` |
@@ -32,12 +40,14 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Refactoring code | `tdd` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
| Running makemigrations or pgmakemigrations | `django-migration-psql` |
| Testing RLS tenant isolation | `prowler-test-api` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
| Working on task | `tdd` |
| Writing Prowler API tests | `prowler-test-api` |
| Writing Python tests with pytest | `pytest` |
| Writing data backfill or data migration | `django-migration-psql` |
---
+40
View File
@@ -2,6 +2,46 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.23.0] (Prowler UNRELEASED)
### 🔐 Security
- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165)
---
## [1.22.1] (Prowler v5.21.1)
### 🐞 Fixed
- Threat score aggregation query to eliminate unnecessary JOINs and `COUNT(DISTINCT)` overhead [(#10394)](https://github.com/prowler-cloud/prowler/pull/10394)
---
## [1.22.0] (Prowler v5.21.0)
### 🚀 Added
- `CORS_ALLOWED_ORIGINS` configurable via environment variable [(#10355)](https://github.com/prowler-cloud/prowler/pull/10355)
- Finding groups support `check_title` substring filtering [(#10377)](https://github.com/prowler-cloud/prowler/pull/10377)
- Attack Paths: Tenant and provider related labels to the nodes so they can be easily filtered on custom queries [(#10308)](https://github.com/prowler-cloud/prowler/pull/10308)
### 🔄 Changed
- Attack Paths: Complete migration to private graph labels and properties, removing deprecated dual-write support [(#10268)](https://github.com/prowler-cloud/prowler/pull/10268)
- Attack Paths: Reduce sync and findings memory usage with smaller batches, cursor iteration, and sequential sessions [(#10359)](https://github.com/prowler-cloud/prowler/pull/10359)
### 🐞 Fixed
- Attack Paths: Recover `graph_data_ready` flag when scan fails during graph swap, preventing query endpoints from staying blocked until the next successful scan [(#10354)](https://github.com/prowler-cloud/prowler/pull/10354)
### 🔐 Security
- Use `psycopg2.sql` to safely compose DDL in `PostgresEnumMigration`, preventing SQL injection via f-string interpolation [(#10166)](https://github.com/prowler-cloud/prowler/pull/10166)
- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165)
---
## [1.21.0] (Prowler v5.20.0)
### 🔄 Changed
+21 -379
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -22,9 +22,10 @@ dependencies = [
"drf-nested-routers (>=0.94.1,<1.0.0)",
"drf-spectacular==0.27.2",
"drf-spectacular-jsonapi==0.5.1",
"defusedxml==0.7.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.20",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
@@ -49,7 +50,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.21.1"
version = "1.23.0"
[project.scripts]
celery = "src.backend.config.settings.celery"
+30 -10
View File
@@ -1,25 +1,22 @@
import atexit
import logging
import threading
from typing import Any
from contextlib import contextmanager
from typing import Iterator
from typing import Any, Iterator
from uuid import UUID
import neo4j
import neo4j.exceptions
from django.conf import settings
from api.attack_paths.retryable_session import RetryableSession
from config.env import env
from django.conf import settings
from tasks.jobs.attack_paths.config import (
BATCH_SIZE,
DEPRECATED_PROVIDER_RESOURCE_LABEL,
PROVIDER_ID_PROPERTY,
PROVIDER_RESOURCE_LABEL,
)
from api.attack_paths.retryable_session import RetryableSession
# Without this Celery goes crazy with Neo4j logging
logging.getLogger("neo4j").setLevel(logging.ERROR)
logging.getLogger("neo4j").propagate = False
@@ -178,7 +175,7 @@ def drop_subgraph(database: str, provider_id: str) -> int:
while deleted_count > 0:
result = session.run(
f"""
MATCH (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL} {{provider_id: $provider_id}})
MATCH (n:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ID_PROPERTY}: $provider_id}})
WITH n LIMIT $batch_size
DETACH DELETE n
RETURN COUNT(n) AS deleted_nodes_count
@@ -196,6 +193,29 @@ def drop_subgraph(database: str, provider_id: str) -> int:
return deleted_nodes
def has_provider_data(database: str, provider_id: str) -> bool:
"""
Check if any ProviderResource node exists for this provider.
Returns `False` if the database doesn't exist.
"""
query = (
f"MATCH (n:{PROVIDER_RESOURCE_LABEL} "
f"{{{PROVIDER_ID_PROPERTY}: $provider_id}}) "
"RETURN 1 LIMIT 1"
)
try:
with get_session(database, default_access_mode=neo4j.READ_ACCESS) as session:
result = session.run(query, {"provider_id": provider_id})
return result.single() is not None
except GraphDatabaseQueryException as exc:
if exc.code == "Neo.ClientError.Database.DatabaseNotFound":
return False
raise
def clear_cache(database: str) -> None:
query = "CALL db.clearQueryCaches()"
@@ -3,7 +3,7 @@ from api.attack_paths.queries.types import (
AttackPathsQueryDefinition,
AttackPathsQueryParameterDefinition,
)
from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL
from tasks.jobs.attack_paths.config import PROVIDER_ID_PROPERTY, PROWLER_FINDING_LABEL
# Custom Attack Path Queries
@@ -16,7 +16,7 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition(
description="Detect EC2 instances with SSH exposed to the internet that can assume higher-privileged roles to read tagged sensitive S3 buckets despite bucket-level public access blocks.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}})
MATCH path_s3 = (aws:AWSAccount {{id: $provider_uid}})--(s3:S3Bucket)--(t:AWSTag)
WHERE toLower(t.key) = toLower($tag_key) AND toLower(t.value) = toLower($tag_value)
@@ -179,7 +179,7 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition(
description="Find EC2 instances flagged as exposed to the internet within the selected account.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}})
MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance)
WHERE ec2.exposed_internet = true
@@ -201,7 +201,7 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition(
description="Find internet-facing resources associated with security groups that allow inbound access from '0.0.0.0/0'.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}})
// Match EC2 instances that are internet-exposed with open security groups (0.0.0.0/0)
MATCH path_ec2 = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance)--(sg:EC2SecurityGroup)--(ipi:IpPermissionInbound)--(ir:IpRange)
@@ -225,7 +225,7 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition(
description="Find Classic Load Balancers exposed to the internet along with their listeners.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}})
MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(elb:LoadBalancer)--(listener:ELBListener)
WHERE elb.exposed_internet = true
@@ -247,7 +247,7 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition(
description="Find ELBv2 load balancers exposed to the internet along with their listeners.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}})
MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(elbv2:LoadBalancerV2)--(listener:ELBV2Listener)
WHERE elbv2.exposed_internet = true
@@ -269,7 +269,7 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition(
description="Given a public IP address, find the related AWS resource and its adjacent node within the selected account.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}})
MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x)-[q]-(y)
WHERE (x:EC2PrivateIp AND x.public_ip = $ip)
@@ -1,7 +1,7 @@
from tasks.jobs.attack_paths.config import DEPRECATED_PROVIDER_RESOURCE_LABEL
from tasks.jobs.attack_paths.config import PROVIDER_ID_PROPERTY, PROVIDER_RESOURCE_LABEL
CARTOGRAPHY_SCHEMA_METADATA = f"""
MATCH (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL} {{provider_id: $provider_id}})
MATCH (n:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ID_PROPERTY}: $provider_id}})
WHERE n._module_name STARTS WITH 'cartography:'
AND NOT n._module_name IN ['cartography:ontology', 'cartography:prowler']
AND n._module_version IS NOT NULL
@@ -13,7 +13,12 @@ from api.attack_paths.queries.schema import (
RAW_SCHEMA_URL,
)
from config.custom_logging import BackendLogger
from tasks.jobs.attack_paths.config import INTERNAL_LABELS, INTERNAL_PROPERTIES
from tasks.jobs.attack_paths.config import (
INTERNAL_LABELS,
INTERNAL_PROPERTIES,
PROVIDER_ID_PROPERTY,
is_dynamic_isolation_label,
)
logger = logging.getLogger(BackendLogger.API)
@@ -253,7 +258,7 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
nodes = []
kept_node_ids = set()
for node in graph.nodes:
if node._properties.get("provider_id") != provider_id:
if node._properties.get(PROVIDER_ID_PROPERTY) != provider_id:
continue
kept_node_ids.add(node.element_id)
@@ -273,7 +278,7 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
relationships = []
for relationship in graph.relationships:
if relationship._properties.get("provider_id") != provider_id:
if relationship._properties.get(PROVIDER_ID_PROPERTY) != provider_id:
continue
if (
@@ -301,7 +306,11 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
def _filter_labels(labels: Iterable[str]) -> list[str]:
return [label for label in labels if label not in INTERNAL_LABELS]
return [
label
for label in labels
if label not in INTERNAL_LABELS and not is_dynamic_isolation_label(label)
]
def _serialize_properties(properties: dict[str, Any]) -> dict[str, Any]:
+12 -3
View File
@@ -18,6 +18,7 @@ from django.db import (
)
from django_celery_beat.models import PeriodicTask
from psycopg2 import connect as psycopg2_connect
from psycopg2 import sql as psycopg2_sql
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
from rest_framework_json_api.serializers import ValidationError
@@ -280,15 +281,23 @@ class PostgresEnumMigration:
self.enum_values = enum_values
def create_enum_type(self, apps, schema_editor): # noqa: F841
string_enum_values = ", ".join([f"'{value}'" for value in self.enum_values])
with schema_editor.connection.cursor() as cursor:
cursor.execute(
f"CREATE TYPE {self.enum_name} AS ENUM ({string_enum_values});"
psycopg2_sql.SQL("CREATE TYPE {} AS ENUM ({})").format(
psycopg2_sql.Identifier(self.enum_name),
psycopg2_sql.SQL(", ").join(
psycopg2_sql.Literal(v) for v in self.enum_values
),
)
)
def drop_enum_type(self, apps, schema_editor): # noqa: F841
with schema_editor.connection.cursor() as cursor:
cursor.execute(f"DROP TYPE {self.enum_name};")
cursor.execute(
psycopg2_sql.SQL("DROP TYPE {}").format(
psycopg2_sql.Identifier(self.enum_name)
)
)
class PostgresEnumField(models.Field):
+6
View File
@@ -926,6 +926,9 @@ class FindingGroupSummaryFilter(FilterSet):
check_id = CharFilter(field_name="check_id", lookup_expr="exact")
check_id__in = CharInFilter(field_name="check_id", lookup_expr="in")
check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains")
check_title__icontains = CharFilter(
field_name="check_title", lookup_expr="icontains"
)
# Provider filters
provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact")
@@ -1025,6 +1028,9 @@ class LatestFindingGroupSummaryFilter(FilterSet):
check_id = CharFilter(field_name="check_id", lookup_expr="exact")
check_id__in = CharInFilter(field_name="check_id", lookup_expr="in")
check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains")
check_title__icontains = CharFilter(
field_name="check_title", lookup_expr="icontains"
)
# Provider filters
provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact")
@@ -0,0 +1,31 @@
# Generated by Django 5.1.15 on 2026-03-18
from django.contrib.postgres.indexes import GinIndex, OpClass
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations
from django.db.models.functions import Upper
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0084_googleworkspace_provider"),
]
operations = [
AddIndexConcurrently(
model_name="findinggroupdailysummary",
index=GinIndex(
OpClass(Upper("check_id"), name="gin_trgm_ops"),
name="fgds_check_id_trgm_idx",
),
),
AddIndexConcurrently(
model_name="findinggroupdailysummary",
index=GinIndex(
OpClass(Upper("check_title"), name="gin_trgm_ops"),
name="fgds_check_title_trgm_idx",
),
),
]
+13 -1
View File
@@ -1,7 +1,6 @@
import json
import logging
import re
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from uuid import UUID, uuid4
@@ -9,6 +8,8 @@ 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
from django.contrib.postgres.fields import ArrayField
@@ -1783,6 +1784,15 @@ class FindingGroupDailySummary(RowLevelSecurityProtectedModel):
fields=["tenant_id", "provider", "inserted_at"],
name="fgds_tenant_prov_ins_idx",
),
# Trigram indexes for case-insensitive search
GinIndex(
OpClass(Upper("check_id"), name="gin_trgm_ops"),
name="fgds_check_id_trgm_idx",
),
GinIndex(
OpClass(Upper("check_title"), name="gin_trgm_ops"),
name="fgds_check_title_trgm_idx",
),
]
class JSONAPIMeta:
@@ -2058,6 +2068,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
root = ET.fromstring(self.metadata_xml)
except ET.ParseError as e:
raise ValidationError({"metadata_xml": f"Invalid XML: {e}"})
except defusedxml.DefusedXmlException as e:
raise ValidationError({"metadata_xml": f"Unsafe XML content rejected: {e}"})
# Entity ID
entity_id = root.attrib.get("entityID")
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.21.1
version: 1.23.0
description: |-
Prowler API specification.
@@ -301,7 +301,7 @@ class TestTokenSwitchTenant:
assert invalid_tenant_response.status_code == 400
assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid"
assert invalid_tenant_response.json()["errors"][0]["detail"] == (
"Tenant does not exist or user is not a " "member."
"Tenant does not exist or user is not a member."
)
@@ -912,10 +912,9 @@ class TestAPIKeyLifecycle:
auth_response = client.get(reverse("provider-list"), headers=api_key_headers)
# Must return 401 Unauthorized, not 500 Internal Server Error
assert auth_response.status_code == 401, (
f"Expected 401 but got {auth_response.status_code}: "
f"{auth_response.json()}"
)
assert (
auth_response.status_code == 401
), f"Expected 401 but got {auth_response.status_code}: {auth_response.json()}"
# Verify error message is present
response_json = auth_response.json()
+17 -13
View File
@@ -10,11 +10,11 @@ from django.conf import settings
import api
import api.apps as api_apps_module
from api.apps import (
ApiConfig,
PRIVATE_KEY_FILE,
PUBLIC_KEY_FILE,
SIGNING_KEY_ENV,
VERIFYING_KEY_ENV,
ApiConfig,
)
@@ -187,9 +187,10 @@ def test_ready_initializes_driver_for_api_process(monkeypatch):
_set_argv(monkeypatch, ["gunicorn"])
_set_testing(monkeypatch, False)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
with (
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
patch("api.attack_paths.database.init_driver") as init_driver,
):
config.ready()
init_driver.assert_called_once()
@@ -200,9 +201,10 @@ def test_ready_skips_driver_for_celery(monkeypatch):
_set_argv(monkeypatch, ["celery", "-A", "api"])
_set_testing(monkeypatch, False)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
with (
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
patch("api.attack_paths.database.init_driver") as init_driver,
):
config.ready()
init_driver.assert_not_called()
@@ -213,9 +215,10 @@ def test_ready_skips_driver_for_manage_py_skip_command(monkeypatch):
_set_argv(monkeypatch, ["manage.py", "migrate"])
_set_testing(monkeypatch, False)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
with (
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
patch("api.attack_paths.database.init_driver") as init_driver,
):
config.ready()
init_driver.assert_not_called()
@@ -226,9 +229,10 @@ def test_ready_skips_driver_when_testing(monkeypatch):
_set_argv(monkeypatch, ["gunicorn"])
_set_testing(monkeypatch, True)
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
"api.attack_paths.database.init_driver"
) as init_driver:
with (
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
patch("api.attack_paths.database.init_driver") as init_driver,
):
config.ready()
init_driver.assert_not_called()
+25 -15
View File
@@ -9,6 +9,10 @@ from rest_framework.exceptions import APIException, PermissionDenied, Validation
from api.attack_paths import database as graph_database
from api.attack_paths import views_helpers
from tasks.jobs.attack_paths.config import (
PROVIDER_ELEMENT_ID_PROPERTY,
PROVIDER_ID_PROPERTY,
)
def _make_neo4j_error(message, code):
@@ -108,7 +112,7 @@ def test_execute_query_serializes_graph(
labels=["AWSAccount"],
properties={
"name": "account",
"provider_id": provider_id,
PROVIDER_ID_PROPERTY: provider_id,
"complex": {
"items": [
attack_paths_graph_stub_classes.NativeValue("value"),
@@ -118,14 +122,14 @@ def test_execute_query_serializes_graph(
},
)
node_2 = attack_paths_graph_stub_classes.Node(
"node-2", ["RDSInstance"], {"provider_id": provider_id}
"node-2", ["RDSInstance"], {PROVIDER_ID_PROPERTY: provider_id}
)
relationship = attack_paths_graph_stub_classes.Relationship(
element_id="rel-1",
rel_type="OWNS",
start_node=node,
end_node=node_2,
properties={"weight": 1, "provider_id": provider_id},
properties={"weight": 1, PROVIDER_ID_PROPERTY: provider_id},
)
graph = SimpleNamespace(nodes=[node, node_2], relationships=[relationship])
@@ -213,20 +217,20 @@ def test_serialize_graph_filters_by_provider_id(attack_paths_graph_stub_classes)
provider_id = "provider-keep"
node_keep = attack_paths_graph_stub_classes.Node(
"n1", ["AWSAccount"], {"provider_id": provider_id}
"n1", ["AWSAccount"], {PROVIDER_ID_PROPERTY: provider_id}
)
node_drop = attack_paths_graph_stub_classes.Node(
"n2", ["AWSAccount"], {"provider_id": "provider-other"}
"n2", ["AWSAccount"], {PROVIDER_ID_PROPERTY: "provider-other"}
)
rel_keep = attack_paths_graph_stub_classes.Relationship(
"r1", "OWNS", node_keep, node_keep, {"provider_id": provider_id}
"r1", "OWNS", node_keep, node_keep, {PROVIDER_ID_PROPERTY: provider_id}
)
rel_drop_by_provider = attack_paths_graph_stub_classes.Relationship(
"r2", "OWNS", node_keep, node_drop, {"provider_id": "provider-other"}
"r2", "OWNS", node_keep, node_drop, {PROVIDER_ID_PROPERTY: "provider-other"}
)
rel_drop_orphaned = attack_paths_graph_stub_classes.Relationship(
"r3", "OWNS", node_keep, node_drop, {"provider_id": provider_id}
"r3", "OWNS", node_keep, node_drop, {PROVIDER_ID_PROPERTY: provider_id}
)
graph = SimpleNamespace(
@@ -350,10 +354,8 @@ def test_serialize_properties_filters_internal_fields():
"_module_name": "cartography:aws",
"_module_version": "0.98.0",
# Provider isolation
"_provider_id": "42",
"_provider_element_id": "42:abc123",
"provider_id": "42",
"provider_element_id": "42:abc123",
PROVIDER_ID_PROPERTY: "42",
PROVIDER_ELEMENT_ID_PROPERTY: "42:abc123",
}
result = views_helpers._serialize_properties(properties)
@@ -361,6 +363,14 @@ def test_serialize_properties_filters_internal_fields():
assert result == {"name": "prod"}
def test_filter_labels_strips_dynamic_isolation_labels():
labels = ["AWSRole", "_Tenant_abc123", "_Provider_def456", "_ProviderResource"]
result = views_helpers._filter_labels(labels)
assert result == ["AWSRole"]
def test_serialize_graph_as_text_node_without_properties():
graph = {
"nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}],
@@ -440,13 +450,13 @@ def test_execute_custom_query_serializes_graph(
):
provider_id = "test-provider-123"
node_1 = attack_paths_graph_stub_classes.Node(
"node-1", ["AWSAccount"], {"provider_id": provider_id}
"node-1", ["AWSAccount"], {PROVIDER_ID_PROPERTY: provider_id}
)
node_2 = attack_paths_graph_stub_classes.Node(
"node-2", ["RDSInstance"], {"provider_id": provider_id}
"node-2", ["RDSInstance"], {PROVIDER_ID_PROPERTY: provider_id}
)
relationship = attack_paths_graph_stub_classes.Relationship(
"rel-1", "OWNS", node_1, node_2, {"provider_id": provider_id}
"rel-1", "OWNS", node_1, node_2, {PROVIDER_ID_PROPERTY: provider_id}
)
graph_result = MagicMock()
@@ -442,3 +442,78 @@ class TestThreadSafety:
# All threads got the same driver instance
assert all(r is mock_driver for r in results)
assert len(results) == 10
class TestHasProviderData:
"""Test has_provider_data helper for checking provider nodes in Neo4j."""
def test_returns_true_when_nodes_exist(self):
import api.attack_paths.database as db_module
mock_session = MagicMock()
mock_result = MagicMock()
mock_result.single.return_value = MagicMock() # non-None record
mock_session.run.return_value = mock_result
session_ctx = MagicMock()
session_ctx.__enter__.return_value = mock_session
session_ctx.__exit__.return_value = False
with patch(
"api.attack_paths.database.get_session",
return_value=session_ctx,
):
assert db_module.has_provider_data("db-tenant-abc", "provider-123") is True
mock_session.run.assert_called_once()
def test_returns_false_when_no_nodes(self):
import api.attack_paths.database as db_module
mock_session = MagicMock()
mock_result = MagicMock()
mock_result.single.return_value = None
mock_session.run.return_value = mock_result
session_ctx = MagicMock()
session_ctx.__enter__.return_value = mock_session
session_ctx.__exit__.return_value = False
with patch(
"api.attack_paths.database.get_session",
return_value=session_ctx,
):
assert db_module.has_provider_data("db-tenant-abc", "provider-123") is False
def test_returns_false_when_database_not_found(self):
import api.attack_paths.database as db_module
session_ctx = MagicMock()
session_ctx.__enter__.side_effect = db_module.GraphDatabaseQueryException(
message="Database does not exist",
code="Neo.ClientError.Database.DatabaseNotFound",
)
with patch(
"api.attack_paths.database.get_session",
return_value=session_ctx,
):
assert (
db_module.has_provider_data("db-tenant-gone", "provider-123") is False
)
def test_raises_on_other_errors(self):
import api.attack_paths.database as db_module
session_ctx = MagicMock()
session_ctx.__enter__.side_effect = db_module.GraphDatabaseQueryException(
message="Connection refused",
code="Neo.TransientError.General.UnknownError",
)
with patch(
"api.attack_paths.database.get_session",
return_value=session_ctx,
):
with pytest.raises(db_module.GraphDatabaseQueryException):
db_module.has_provider_data("db-tenant-abc", "provider-123")
@@ -6,10 +6,12 @@ import pytest
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS, OperationalError
from freezegun import freeze_time
from psycopg2 import sql as psycopg2_sql
from rest_framework_json_api.serializers import ValidationError
from api.db_utils import (
POSTGRES_TENANT_VAR,
PostgresEnumMigration,
_should_create_index_on_partition,
batch_delete,
create_objects_in_batches,
@@ -910,3 +912,61 @@ class TestRlsTransaction:
cursor.execute("SELECT 1")
result = cursor.fetchone()
assert result[0] == 1
class TestPostgresEnumMigration:
"""
Verify that PostgresEnumMigration builds DDL statements via psycopg2.sql
so that enum type names and values are always properly quoted preventing
SQL injection through f-string interpolation.
"""
def _make_mock_schema_editor(self):
mock_cursor = MagicMock()
mock_conn = MagicMock()
mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor)
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
mock_schema_editor = MagicMock()
mock_schema_editor.connection = mock_conn
return mock_schema_editor, mock_cursor
def test_create_enum_type_generates_correct_sql(self):
"""create_enum_type builds a proper CREATE TYPE … AS ENUM via psycopg2.sql."""
migration = PostgresEnumMigration("my_enum", ("val_a", "val_b"))
schema_editor, mock_cursor = self._make_mock_schema_editor()
migration.create_enum_type(apps=None, schema_editor=schema_editor)
mock_cursor.execute.assert_called_once()
query_arg = mock_cursor.execute.call_args[0][0]
assert isinstance(
query_arg, psycopg2_sql.Composable
), "create_enum_type must pass a psycopg2.sql.Composable, not a raw string."
# Verify the composed SQL structure: CREATE TYPE <Identifier> AS ENUM (<Literals>)
parts = query_arg.seq
assert parts[0] == psycopg2_sql.SQL("CREATE TYPE ")
assert isinstance(parts[1], psycopg2_sql.Identifier)
assert parts[1].strings == ("my_enum",)
assert parts[2] == psycopg2_sql.SQL(" AS ENUM (")
# The enum values are a Composed of Literal items joined by ", "
enum_literals = [p for p in parts[3].seq if isinstance(p, psycopg2_sql.Literal)]
assert [lit._wrapped for lit in enum_literals] == ["val_a", "val_b"]
assert parts[4] == psycopg2_sql.SQL(")")
def test_drop_enum_type_generates_correct_sql(self):
"""drop_enum_type builds a proper DROP TYPE via psycopg2.sql."""
migration = PostgresEnumMigration("my_enum", ("val_a",))
schema_editor, mock_cursor = self._make_mock_schema_editor()
migration.drop_enum_type(apps=None, schema_editor=schema_editor)
mock_cursor.execute.assert_called_once()
query_arg = mock_cursor.execute.call_args[0][0]
assert isinstance(
query_arg, psycopg2_sql.Composable
), "drop_enum_type must pass a psycopg2.sql.Composable, not a raw string."
# Verify the composed SQL structure: DROP TYPE <Identifier>
parts = query_arg.seq
assert parts[0] == psycopg2_sql.SQL("DROP TYPE ")
assert isinstance(parts[1], psycopg2_sql.Identifier)
assert parts[1].strings == ("my_enum",)
+33
View File
@@ -243,6 +243,39 @@ class TestSAMLConfigurationModel:
assert "Invalid XML" in errors["metadata_xml"][0]
assert "not well-formed" in errors["metadata_xml"][0]
def test_xml_bomb_rejected(self, tenants_fixture):
"""
Regression test: a 'billion laughs' XML bomb in the SAML metadata field
must be rejected and not allowed to exhaust server memory / CPU.
Before the fix, xml.etree.ElementTree was used directly, which does not
protect against entity-expansion attacks. The fix switches to defusedxml
which raises an exception for any XML containing entity definitions.
"""
tenant = tenants_fixture[0]
xml_bomb = (
"<?xml version='1.0'?>"
"<!DOCTYPE bomb ["
" <!ENTITY a 'aaaaaaaaaa'>"
" <!ENTITY b '&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;'>"
" <!ENTITY c '&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;'>"
" <!ENTITY d '&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;'>"
"]>"
"<md:EntityDescriptor entityID='&d;' "
"xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata'/>"
)
config = SAMLConfiguration(
email_domain="xmlbomb.com",
metadata_xml=xml_bomb,
tenant=tenant,
)
with pytest.raises(ValidationError) as exc_info:
config._parse_metadata()
errors = exc_info.value.message_dict
assert "metadata_xml" in errors
def test_metadata_missing_sso_fails(self, tenants_fixture):
tenant = tenants_fixture[0]
xml = """<md:EntityDescriptor entityID="x" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
+22 -2
View File
@@ -7858,8 +7858,12 @@ class TestUserRoleRelationshipViewSet:
assert response.status_code == status.HTTP_204_NO_CONTENT
relationships = UserRoleRelationship.objects.filter(user=create_test_user.id)
assert relationships.count() == 4
for relationship in relationships[2:]: # Skip admin role
assert relationship.role.id in [r.id for r in roles_fixture[:2]]
# Use set membership instead of positional slicing — QuerySet ordering is
# non-deterministic without an explicit order_by, which makes slice-based
# checks intermittently fail.
added_role_ids = {r.id for r in roles_fixture[:2]}
relationship_role_ids = {rel.role.id for rel in relationships}
assert added_role_ids.issubset(relationship_role_ids)
def test_create_relationship_already_exists(
self, authenticated_client, roles_fixture, create_test_user
@@ -15522,6 +15526,22 @@ class TestFindingGroupViewSet:
assert len(response.json()["data"]) == 1
assert "bucket" in response.json()["data"][0]["id"].lower()
def test_finding_groups_check_title_icontains(
self, authenticated_client, finding_groups_fixture
):
"""Test searching check titles with icontains."""
response = authenticated_client.get(
reverse("finding-group-list"),
{
"filter[inserted_at]": TODAY,
"filter[check_title.icontains]": "public access",
},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
assert data[0]["id"] == "s3_bucket_public_access"
def test_resources_not_found(self, authenticated_client):
"""Test 404 returned for nonexistent check_id."""
response = authenticated_client.get(
+22 -5
View File
@@ -4,7 +4,6 @@ import json
import logging
import os
import time
from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timedelta, timezone
@@ -12,7 +11,6 @@ from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from urllib.parse import urljoin
import sentry_sdk
from allauth.socialaccount.models import SocialAccount, SocialApp
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
@@ -76,6 +74,7 @@ from rest_framework.exceptions import (
)
from rest_framework.generics import GenericAPIView, get_object_or_404
from rest_framework.permissions import SAFE_METHODS
from rest_framework_json_api import filters as jsonapi_filters
from rest_framework_json_api.views import RelationshipView, Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from tasks.beat import schedule_provider_scan
@@ -100,7 +99,6 @@ from api.attack_paths import database as graph_database
from api.attack_paths import get_queries_for_provider, get_query_by_id
from api.attack_paths import views_helpers as attack_paths_views_helpers
from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
from api.renderers import APIJSONRenderer, PlainTextRenderer
from api.compliance import (
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
get_compliance_frameworks,
@@ -199,6 +197,7 @@ from api.models import (
)
from api.pagination import ComplianceOverviewPagination
from api.rbac.permissions import Permissions, get_providers, get_role
from api.renderers import APIJSONRenderer, PlainTextRenderer
from api.rls import Tenant
from api.utils import (
CustomOAuth2Client,
@@ -408,7 +407,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.21.1"
spectacular_settings.VERSION = "1.23.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -6777,13 +6776,29 @@ class FindingGroupViewSet(BaseRLSViewSet):
queryset = FindingGroupDailySummary.objects.all()
serializer_class = FindingGroupSerializer
filterset_class = FindingGroupSummaryFilter
filter_backends = [
jsonapi_filters.QueryParameterValidationFilter,
jsonapi_filters.OrderingFilter,
CustomDjangoFilterBackend,
]
http_method_names = ["get"]
required_permissions = []
def get_filterset_class(self):
"""Return appropriate filter based on action."""
"""Return the filterset class used for schema generation and the list action.
Note: The resources and latest_resources actions do not use this method
at runtime. They manually instantiate FindingGroupFilter /
LatestFindingGroupFilter against a Finding queryset (see
_get_finding_queryset). The class returned here for those actions only
affects the OpenAPI schema generated by drf-spectacular.
"""
if self.action == "latest":
return LatestFindingGroupSummaryFilter
if self.action == "resources":
return FindingGroupFilter
if self.action == "latest_resources":
return LatestFindingGroupFilter
return FindingGroupSummaryFilter
def get_queryset(self):
@@ -7237,6 +7252,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
and timing information including how long they have been failing.
""",
tags=["Finding Groups"],
filters=True,
)
@action(detail=True, methods=["get"], url_path="resources")
def resources(self, request, pk=None):
@@ -7311,6 +7327,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
and timing information. No date filters required.
""",
tags=["Finding Groups"],
filters=True,
)
@action(
detail=False,
@@ -3,6 +3,10 @@ from config.env import env
DEBUG = env.bool("DJANGO_DEBUG", default=False)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
CORS_ALLOWED_ORIGINS = env.list(
"DJANGO_CORS_ALLOWED_ORIGINS",
default=["http://localhost", "http://127.0.0.1"],
)
# Database
# TODO Use Django database routers https://docs.djangoproject.com/en/5.0/topics/db/multi-db/#automatic-database-routing
@@ -1,25 +1,30 @@
from dataclasses import dataclass
from typing import Callable
from uuid import UUID
from config.env import env
from tasks.jobs.attack_paths import aws
# Batch size for Neo4j operations
# Batch size for Neo4j write operations (resource labeling, cleanup)
BATCH_SIZE = env.int("ATTACK_PATHS_BATCH_SIZE", 1000)
# Batch size for Postgres findings fetch (keyset pagination page size)
FINDINGS_BATCH_SIZE = env.int("ATTACK_PATHS_FINDINGS_BATCH_SIZE", 500)
# Batch size for temp-to-tenant graph sync (nodes and relationships per cursor page)
SYNC_BATCH_SIZE = env.int("ATTACK_PATHS_SYNC_BATCH_SIZE", 250)
# Neo4j internal labels (Prowler-specific, not provider-specific)
# - `Internet`: Singleton node representing external internet access for exposed-resource queries
# - `ProwlerFinding`: Label for finding nodes created by Prowler and linked to cloud resources
# - `_ProviderResource`: Added to ALL synced nodes for provider isolation and drop/query ops
# - `Internet`: Singleton node representing external internet access for exposed-resource queries
INTERNET_NODE_LABEL = "Internet"
PROWLER_FINDING_LABEL = "ProwlerFinding"
PROVIDER_RESOURCE_LABEL = "_ProviderResource"
INTERNET_NODE_LABEL = "Internet"
# Phase 1 dual-write: deprecated label kept for drop_subgraph and infrastructure queries
# Remove in Phase 2 once all nodes use the private label exclusively
DEPRECATED_PROVIDER_RESOURCE_LABEL = "ProviderResource"
# Dynamic isolation labels that contain entity UUIDs and are added to every synced node during sync
# Format: _Tenant_{uuid_no_hyphens}, _Provider_{uuid_no_hyphens}
TENANT_LABEL_PREFIX = "_Tenant_"
PROVIDER_LABEL_PREFIX = "_Provider_"
DYNAMIC_ISOLATION_PREFIXES = [TENANT_LABEL_PREFIX, PROVIDER_LABEL_PREFIX]
@dataclass(frozen=True)
@@ -31,7 +36,6 @@ class ProviderConfig:
uid_field: str # e.g., "arn"
# Label for resources connected to the account node, enabling indexed finding lookups.
resource_label: str # e.g., "_AWSResource"
deprecated_resource_label: str # e.g., "AWSResource"
ingestion_function: Callable
@@ -43,7 +47,6 @@ AWS_CONFIG = ProviderConfig(
root_node_label="AWSAccount",
uid_field="arn",
resource_label="_AWSResource",
deprecated_resource_label="AWSResource",
ingestion_function=aws.start_aws_ingestion,
)
@@ -56,18 +59,16 @@ PROVIDER_CONFIGS: dict[str, ProviderConfig] = {
INTERNAL_LABELS: list[str] = [
"Tenant", # From Cartography, but it looks like it's ours
PROVIDER_RESOURCE_LABEL,
DEPRECATED_PROVIDER_RESOURCE_LABEL,
# Add all provider-specific resource labels
*[config.resource_label for config in PROVIDER_CONFIGS.values()],
*[config.deprecated_resource_label for config in PROVIDER_CONFIGS.values()],
]
# Provider isolation properties
PROVIDER_ID_PROPERTY = "_provider_id"
PROVIDER_ELEMENT_ID_PROPERTY = "_provider_element_id"
PROVIDER_ISOLATION_PROPERTIES: list[str] = [
"_provider_id",
"_provider_element_id",
"provider_id",
"provider_element_id",
PROVIDER_ID_PROPERTY,
PROVIDER_ELEMENT_ID_PROPERTY,
]
# Cartography bookkeeping metadata
@@ -117,7 +118,25 @@ def get_provider_resource_label(provider_type: str) -> str:
return config.resource_label if config else "_UnknownProviderResource"
def get_deprecated_provider_resource_label(provider_type: str) -> str:
"""Get the deprecated resource label for a provider type (e.g., `AWSResource`)."""
config = PROVIDER_CONFIGS.get(provider_type)
return config.deprecated_resource_label if config else "UnknownProviderResource"
# Dynamic Isolation Label Helpers
# --------------------------------
def _normalize_uuid(value: str | UUID) -> str:
"""Strip hyphens from a UUID string for use in Neo4j labels."""
return str(value).replace("-", "")
def get_tenant_label(tenant_id: str | UUID) -> str:
"""Get the Neo4j label for a tenant (e.g., `_Tenant_019c41ee7df37deca684d839f95619f8`)."""
return f"{TENANT_LABEL_PREFIX}{_normalize_uuid(tenant_id)}"
def get_provider_label(provider_id: str | UUID) -> str:
"""Get the Neo4j label for a provider (e.g., `_Provider_019c41ee7df37deca684d839f95619f8`)."""
return f"{PROVIDER_LABEL_PREFIX}{_normalize_uuid(provider_id)}"
def is_dynamic_isolation_label(label: str) -> bool:
"""Check if a label is a dynamic tenant/provider isolation label."""
return any(label.startswith(prefix) for prefix in DYNAMIC_ISOLATION_PREFIXES)
@@ -3,15 +3,13 @@ from typing import Any
from cartography.config import Config as CartographyConfig
from celery.utils.log import get_task_logger
from tasks.jobs.attack_paths.config import is_provider_available
from api.attack_paths import database as graph_database
from api.db_utils import rls_transaction
from api.models import (
AttackPathsScan as ProwlerAPIAttackPathsScan,
Provider as ProwlerAPIProvider,
StateChoices,
)
from tasks.jobs.attack_paths.config import is_provider_available
from api.models import AttackPathsScan as ProwlerAPIAttackPathsScan
from api.models import Provider as ProwlerAPIProvider
from api.models import StateChoices
logger = get_task_logger(__name__)
@@ -155,6 +153,37 @@ def set_provider_graph_data_ready(
attack_paths_scan.refresh_from_db(fields=["graph_data_ready"])
def recover_graph_data_ready(
attack_paths_scan: ProwlerAPIAttackPathsScan,
) -> None:
"""
Best-effort recovery of `graph_data_ready` after a scan failure.
Queries Neo4j to check if the provider still has data in the tenant
database. If data exists, restores `graph_data_ready=True` for all scans
of this provider. Never raises.
Trade-off: if the worker crashed mid-sync, partial data may exist and
this will re-enable queries against it. We accept that because leaving
`graph_data_ready=False` permanently (blocking all queries until the
next successful scan) is a worse outcome for the user.
"""
try:
tenant_db = graph_database.get_database_name(attack_paths_scan.tenant_id)
if graph_database.has_provider_data(
tenant_db, str(attack_paths_scan.provider_id)
):
set_provider_graph_data_ready(attack_paths_scan, True)
logger.info(
f"Recovered `graph_data_ready` for provider {attack_paths_scan.provider_id}"
)
except Exception:
logger.exception(
f"Failed to recover `graph_data_ready` for provider {attack_paths_scan.provider_id}"
)
def fail_attack_paths_scan(
tenant_id: str,
scan_id: str,
@@ -185,3 +214,5 @@ def fail_attack_paths_scan(
StateChoices.FAILED,
{"global_error": error},
)
recover_graph_data_ready(attack_paths_scan)
@@ -9,23 +9,15 @@ This module handles:
"""
from collections import defaultdict
from dataclasses import asdict, dataclass, fields
from typing import Any, Generator
from uuid import UUID
import neo4j
from cartography.config import Config as CartographyConfig
from celery.utils.log import get_task_logger
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding as FindingModel
from api.models import Provider, ResourceFindingMapping
from prowler.config import config as ProwlerConfig
from tasks.jobs.attack_paths.config import (
BATCH_SIZE,
get_deprecated_provider_resource_label,
FINDINGS_BATCH_SIZE,
get_node_uid_field,
get_provider_resource_label,
get_root_node_label,
@@ -38,75 +30,54 @@ from tasks.jobs.attack_paths.queries import (
render_cypher_template,
)
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding as FindingModel
from api.models import Provider, ResourceFindingMapping
from prowler.config import config as ProwlerConfig
logger = get_task_logger(__name__)
# Type Definitions
# -----------------
# Maps dataclass field names to Django ORM query field names
_DB_FIELD_MAP: dict[str, str] = {
"check_title": "check_metadata__checktitle",
}
# Django ORM field names for `.values()` queries
# Most map 1:1 to Neo4j property names, exceptions are remapped in `_to_neo4j_dict`
_DB_QUERY_FIELDS = [
"id",
"uid",
"inserted_at",
"updated_at",
"first_seen_at",
"scan_id",
"delta",
"status",
"status_extended",
"severity",
"check_id",
"check_metadata__checktitle",
"muted",
"muted_reason",
]
@dataclass(slots=True)
class Finding:
"""
Finding data for Neo4j ingestion.
Can be created from a Django .values() query result using from_db_record().
"""
id: str
uid: str
inserted_at: str
updated_at: str
first_seen_at: str
scan_id: str
delta: str
status: str
status_extended: str
severity: str
check_id: str
check_title: str
muted: bool
muted_reason: str | None
resource_uid: str | None = None
@classmethod
def get_db_query_fields(cls) -> tuple[str, ...]:
"""Get field names for Django .values() query."""
return tuple(
_DB_FIELD_MAP.get(f.name, f.name)
for f in fields(cls)
if f.name != "resource_uid"
)
@classmethod
def from_db_record(cls, record: dict[str, Any], resource_uid: str) -> "Finding":
"""Create a Finding from a Django .values() query result."""
return cls(
id=str(record["id"]),
uid=record["uid"],
inserted_at=record["inserted_at"],
updated_at=record["updated_at"],
first_seen_at=record["first_seen_at"],
scan_id=str(record["scan_id"]),
delta=record["delta"],
status=record["status"],
status_extended=record["status_extended"],
severity=record["severity"],
check_id=str(record["check_id"]),
check_title=record["check_metadata__checktitle"],
muted=record["muted"],
muted_reason=record["muted_reason"],
resource_uid=resource_uid,
)
def to_dict(self) -> dict[str, Any]:
"""Convert to dict for Neo4j ingestion."""
return asdict(self)
def _to_neo4j_dict(record: dict[str, Any], resource_uid: str) -> dict[str, Any]:
"""Transform a Django `.values()` record into a `dict` ready for Neo4j ingestion."""
return {
"id": str(record["id"]),
"uid": record["uid"],
"inserted_at": record["inserted_at"],
"updated_at": record["updated_at"],
"first_seen_at": record["first_seen_at"],
"scan_id": str(record["scan_id"]),
"delta": record["delta"],
"status": record["status"],
"status_extended": record["status_extended"],
"severity": record["severity"],
"check_id": str(record["check_id"]),
"check_title": record["check_metadata__checktitle"],
"muted": record["muted"],
"muted_reason": record["muted_reason"],
"resource_uid": resource_uid,
}
# Public API
@@ -153,9 +124,6 @@ def add_resource_label(
{
"__ROOT_LABEL__": get_root_node_label(provider_type),
"__RESOURCE_LABEL__": get_provider_resource_label(provider_type),
"__DEPRECATED_RESOURCE_LABEL__": get_deprecated_provider_resource_label(
provider_type
),
},
)
@@ -184,7 +152,7 @@ def add_resource_label(
def load_findings(
neo4j_session: neo4j.Session,
findings_batches: Generator[list[Finding], None, None],
findings_batches: Generator[list[dict[str, Any]], None, None],
prowler_api_provider: Provider,
config: CartographyConfig,
) -> None:
@@ -213,7 +181,7 @@ def load_findings(
batch_size = len(batch)
total_records += batch_size
parameters["findings_data"] = [f.to_dict() for f in batch]
parameters["findings_data"] = batch
logger.info(f"Loading findings batch {batch_num} ({batch_size} records)")
neo4j_session.run(query, parameters)
@@ -251,16 +219,17 @@ def cleanup_findings(
def stream_findings_with_resources(
prowler_api_provider: Provider,
scan_id: str,
) -> Generator[list[Finding], None, None]:
) -> Generator[list[dict[str, Any]], None, None]:
"""
Stream findings with their associated resources in batches.
Uses keyset pagination for efficient traversal of large datasets.
Memory efficient: yields one batch at a time, never holds all findings in memory.
Memory efficient: yields one batch at a time as dicts ready for Neo4j ingestion,
never holds all findings in memory.
"""
logger.info(
f"Starting findings stream for scan {scan_id} "
f"(tenant {prowler_api_provider.tenant_id}) with batch size {BATCH_SIZE}"
f"(tenant {prowler_api_provider.tenant_id}) with batch size {FINDINGS_BATCH_SIZE}"
)
tenant_id = prowler_api_provider.tenant_id
@@ -309,15 +278,14 @@ def _fetch_findings_batch(
Uses read replica and RLS-scoped transaction.
"""
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Use all_objects to avoid the ActiveProviderManager's implicit JOIN
# through Scan -> Provider (to check is_deleted=False).
# The provider is already validated as active in this context.
# 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")
if after_id is not None:
qs = qs.filter(id__gt=after_id)
return list(qs.values(*Finding.get_db_query_fields())[:BATCH_SIZE])
return list(qs.values(*_DB_QUERY_FIELDS)[:FINDINGS_BATCH_SIZE])
# Batch Enrichment
@@ -327,7 +295,7 @@ def _fetch_findings_batch(
def _enrich_batch_with_resources(
findings_batch: list[dict[str, Any]],
tenant_id: str,
) -> list[Finding]:
) -> list[dict[str, Any]]:
"""
Enrich findings with their resource UIDs.
@@ -338,7 +306,7 @@ def _enrich_batch_with_resources(
resource_map = _build_finding_resource_map(finding_ids, tenant_id)
return [
Finding.from_db_record(finding, resource_uid)
_to_neo4j_dict(finding, resource_uid)
for finding in findings_batch
for resource_uid in resource_map.get(finding["id"], [])
]
@@ -6,9 +6,10 @@ from cartography.client.core.tx import run_write_query
from celery.utils.log import get_task_logger
from tasks.jobs.attack_paths.config import (
DEPRECATED_PROVIDER_RESOURCE_LABEL,
INTERNET_NODE_LABEL,
PROWLER_FINDING_LABEL,
PROVIDER_ELEMENT_ID_PROPERTY,
PROVIDER_ID_PROPERTY,
PROVIDER_RESOURCE_LABEL,
)
@@ -27,8 +28,6 @@ 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);",
"CREATE INDEX deprecated_aws_resource_arn IF NOT EXISTS FOR (n:AWSResource) ON (n.arn);",
"CREATE INDEX deprecated_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_provider_uid IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.provider_uid);",
@@ -40,10 +39,8 @@ FINDINGS_INDEX_STATEMENTS = [
# Indexes for provider resource sync operations
SYNC_INDEX_STATEMENTS = [
f"CREATE INDEX provider_element_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n._provider_element_id);",
f"CREATE INDEX provider_resource_provider_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n._provider_id);",
f"CREATE INDEX deprecated_provider_element_id IF NOT EXISTS FOR (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL}) ON (n.provider_element_id);",
f"CREATE INDEX deprecated_provider_resource_provider_id IF NOT EXISTS FOR (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL}) ON (n.provider_id);",
f"CREATE INDEX provider_resource_element_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n.{PROVIDER_ELEMENT_ID_PROPERTY});",
f"CREATE INDEX provider_resource_provider_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n.{PROVIDER_ID_PROPERTY});",
]
@@ -2,6 +2,8 @@
from tasks.jobs.attack_paths.config import (
INTERNET_NODE_LABEL,
PROWLER_FINDING_LABEL,
PROVIDER_ELEMENT_ID_PROPERTY,
PROVIDER_ID_PROPERTY,
PROVIDER_RESOURCE_LABEL,
)
@@ -26,7 +28,7 @@ ADD_RESOURCE_LABEL_TEMPLATE = """
MATCH (account:__ROOT_LABEL__ {id: $provider_uid})-->(r)
WHERE NOT r:__ROOT_LABEL__ AND NOT r:__RESOURCE_LABEL__
WITH r LIMIT $batch_size
SET r:__RESOURCE_LABEL__:__DEPRECATED_RESOURCE_LABEL__
SET r:__RESOURCE_LABEL__
RETURN COUNT(r) AS labeled_count
"""
@@ -149,22 +151,18 @@ RELATIONSHIPS_FETCH_QUERY = """
LIMIT $batch_size
"""
NODE_SYNC_TEMPLATE = """
NODE_SYNC_TEMPLATE = f"""
UNWIND $rows AS row
MERGE (n:__NODE_LABELS__ {_provider_element_id: row.provider_element_id})
MERGE (n:__NODE_LABELS__ {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.provider_element_id}})
SET n += row.props
SET n._provider_id = $provider_id
SET n.provider_element_id = row.provider_element_id
SET n.provider_id = $provider_id
""" # The last two lines are deprecated properties
SET n.{PROVIDER_ID_PROPERTY} = $provider_id
"""
RELATIONSHIP_SYNC_TEMPLATE = f"""
UNWIND $rows AS row
MATCH (s:{PROVIDER_RESOURCE_LABEL} {{_provider_element_id: row.start_element_id}})
MATCH (t:{PROVIDER_RESOURCE_LABEL} {{_provider_element_id: row.end_element_id}})
MERGE (s)-[r:__REL_TYPE__ {{_provider_element_id: row.provider_element_id}}]->(t)
MATCH (s:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.start_element_id}})
MATCH (t:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.end_element_id}})
MERGE (s)-[r:__REL_TYPE__ {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.provider_element_id}}]->(t)
SET r += row.props
SET r._provider_id = $provider_id
SET r.provider_element_id = row.provider_element_id
SET r.provider_id = $provider_id
""" # The last two lines are deprecated properties
SET r.{PROVIDER_ID_PROPERTY} = $provider_id
"""
+31 -10
View File
@@ -55,7 +55,6 @@ exception propagates to Celery.
import logging
import time
from typing import Any
from cartography.config import Config as CartographyConfig
@@ -63,16 +62,14 @@ from cartography.intel import analysis as cartography_analysis
from cartography.intel import create_indexes as cartography_create_indexes
from cartography.intel import ontology as cartography_ontology
from celery.utils.log import get_task_logger
from tasks.jobs.attack_paths import db_utils, findings, internet, sync, utils
from tasks.jobs.attack_paths.config import get_cartography_ingestion_function
from api.attack_paths import database as graph_database
from api.db_utils import rls_transaction
from api.models import (
Provider as ProwlerAPIProvider,
StateChoices,
)
from api.models import Provider as ProwlerAPIProvider
from api.models import StateChoices
from api.utils import initialize_prowler_provider
from tasks.jobs.attack_paths import db_utils, findings, internet, sync, utils
from tasks.jobs.attack_paths.config import get_cartography_ingestion_function
# Without this Celery goes crazy with Cartography logging
logging.getLogger("cartography").setLevel(logging.ERROR)
@@ -147,6 +144,10 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
attack_paths_scan, task_id, tenant_cartography_config
)
subgraph_dropped = False
sync_completed = False
provider_gated = False
try:
logger.info(
f"Creating Neo4j database {tmp_cartography_config.neo4j_database} for tenant {prowler_api_provider.tenant_id}"
@@ -225,10 +226,12 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
logger.info(f"Deleting existing provider graph in {tenant_database_name}")
db_utils.set_provider_graph_data_ready(attack_paths_scan, False)
provider_gated = True
graph_database.drop_subgraph(
database=tenant_database_name,
provider_id=str(prowler_api_provider.id),
)
subgraph_dropped = True
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 98)
logger.info(
@@ -237,8 +240,10 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
sync.sync_graph(
source_database=tmp_database_name,
target_database=tenant_database_name,
tenant_id=str(prowler_api_provider.tenant_id),
provider_id=str(prowler_api_provider.id),
)
sync_completed = True
db_utils.set_graph_data_ready(attack_paths_scan, True)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 99)
@@ -263,23 +268,39 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
logger.exception(exception_message)
ingestion_exceptions["global_error"] = exception_message
# Handling databases changes
# Recover graph_data_ready based on how far the swap got.
# Partial drop (mid-batch failure) may leave `subgraph_dropped=False`
# with data partially deleted, so we prefer that over permanently blocked queries.
try:
if sync_completed:
db_utils.set_graph_data_ready(attack_paths_scan, True)
elif provider_gated and not subgraph_dropped:
db_utils.set_provider_graph_data_ready(attack_paths_scan, True)
except Exception:
logger.error(
f"Failed to recover `graph_data_ready` for provider {attack_paths_scan.provider_id}",
exc_info=True,
)
# Dropping the temporary database if it still exists
try:
graph_database.drop_database(tmp_cartography_config.neo4j_database)
except Exception as e:
logger.error(
f"Failed to drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup: {e}",
f"Failed to drop temporary Neo4j database `{tmp_cartography_config.neo4j_database}` during cleanup: {e}",
exc_info=True,
)
# Set Attack Paths scan state to FAILED
try:
db_utils.finish_attack_paths_scan(
attack_paths_scan, StateChoices.FAILED, ingestion_exceptions
)
except Exception as e:
logger.error(
f"Could not mark attack paths scan {attack_paths_scan.id} as FAILED (row may have been deleted): {e}",
f"Could not mark Attack Paths scan {attack_paths_scan.id} as `FAILED` (row may have been deleted): {e}",
exc_info=True,
)
+85 -66
View File
@@ -8,14 +8,16 @@ to the tenant database, adding provider isolation labels and properties.
from collections import defaultdict
from typing import Any
import neo4j
from celery.utils.log import get_task_logger
from api.attack_paths import database as graph_database
from tasks.jobs.attack_paths.config import (
BATCH_SIZE,
DEPRECATED_PROVIDER_RESOURCE_LABEL,
PROVIDER_ISOLATION_PROPERTIES,
PROVIDER_RESOURCE_LABEL,
SYNC_BATCH_SIZE,
get_provider_label,
get_tenant_label,
)
from tasks.jobs.attack_paths.indexes import IndexType, create_indexes
from tasks.jobs.attack_paths.queries import (
@@ -37,6 +39,7 @@ def create_sync_indexes(neo4j_session) -> None:
def sync_graph(
source_database: str,
target_database: str,
tenant_id: str,
provider_id: str,
) -> dict[str, int]:
"""
@@ -45,6 +48,7 @@ def sync_graph(
Args:
`source_database`: The temporary scan database
`target_database`: The tenant database
`tenant_id`: The tenant ID for isolation
`provider_id`: The provider ID for isolation
Returns:
@@ -53,6 +57,7 @@ def sync_graph(
nodes_synced = sync_nodes(
source_database,
target_database,
tenant_id,
provider_id,
)
relationships_synced = sync_relationships(
@@ -70,50 +75,45 @@ def sync_graph(
def sync_nodes(
source_database: str,
target_database: str,
tenant_id: str,
provider_id: str,
) -> int:
"""
Sync nodes from source to target database.
Adds `_ProviderResource` label and `_provider_id` property to all nodes.
Also adds dynamic `_Tenant_{id}` and `_Provider_{id}` isolation labels.
Source and target sessions are opened sequentially per batch to avoid
holding two Bolt connections simultaneously for the entire sync duration.
"""
last_id = -1
total_synced = 0
with (
graph_database.get_session(source_database) as source_session,
graph_database.get_session(target_database) as target_session,
):
while True:
rows = list(
source_session.run(
NODE_FETCH_QUERY,
{"last_id": last_id, "batch_size": BATCH_SIZE},
)
while True:
grouped: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
batch_count = 0
with graph_database.get_session(source_database) as source_session:
result = source_session.run(
NODE_FETCH_QUERY,
{"last_id": last_id, "batch_size": SYNC_BATCH_SIZE},
)
for record in result:
batch_count += 1
last_id = record["internal_id"]
key, value = _node_to_sync_dict(record, provider_id)
grouped[key].append(value)
if not rows:
break
last_id = rows[-1]["internal_id"]
grouped: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
for row in rows:
labels = tuple(sorted(set(row["labels"] or [])))
props = dict(row["props"] or {})
_strip_internal_properties(props)
provider_element_id = f"{provider_id}:{row['element_id']}"
grouped[labels].append(
{
"provider_element_id": provider_element_id,
"props": props,
}
)
if batch_count == 0:
break
with graph_database.get_session(target_database) as target_session:
for labels, batch in grouped.items():
label_set = set(labels)
label_set.add(PROVIDER_RESOURCE_LABEL)
label_set.add(DEPRECATED_PROVIDER_RESOURCE_LABEL)
label_set.add(get_tenant_label(tenant_id))
label_set.add(get_provider_label(provider_id))
node_labels = ":".join(f"`{label}`" for label in sorted(label_set))
query = render_cypher_template(
@@ -127,10 +127,10 @@ def sync_nodes(
},
)
total_synced += len(rows)
logger.info(
f"Synced {total_synced} nodes from {source_database} to {target_database}"
)
total_synced += batch_count
logger.info(
f"Synced {total_synced} nodes from {source_database} to {target_database}"
)
return total_synced
@@ -144,41 +144,32 @@ def sync_relationships(
Sync relationships from source to target database.
Adds `_provider_id` property to all relationships.
Source and target sessions are opened sequentially per batch to avoid
holding two Bolt connections simultaneously for the entire sync duration.
"""
last_id = -1
total_synced = 0
with (
graph_database.get_session(source_database) as source_session,
graph_database.get_session(target_database) as target_session,
):
while True:
rows = list(
source_session.run(
RELATIONSHIPS_FETCH_QUERY,
{"last_id": last_id, "batch_size": BATCH_SIZE},
)
while True:
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
batch_count = 0
with graph_database.get_session(source_database) as source_session:
result = source_session.run(
RELATIONSHIPS_FETCH_QUERY,
{"last_id": last_id, "batch_size": SYNC_BATCH_SIZE},
)
for record in result:
batch_count += 1
last_id = record["internal_id"]
key, value = _rel_to_sync_dict(record, provider_id)
grouped[key].append(value)
if not rows:
break
last_id = rows[-1]["internal_id"]
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
props = dict(row["props"] or {})
_strip_internal_properties(props)
rel_type = row["rel_type"]
grouped[rel_type].append(
{
"start_element_id": f"{provider_id}:{row['start_element_id']}",
"end_element_id": f"{provider_id}:{row['end_element_id']}",
"provider_element_id": f"{provider_id}:{rel_type}:{row['internal_id']}",
"props": props,
}
)
if batch_count == 0:
break
with graph_database.get_session(target_database) as target_session:
for rel_type, batch in grouped.items():
query = render_cypher_template(
RELATIONSHIP_SYNC_TEMPLATE, {"__REL_TYPE__": rel_type}
@@ -191,14 +182,42 @@ def sync_relationships(
},
)
total_synced += len(rows)
logger.info(
f"Synced {total_synced} relationships from {source_database} to {target_database}"
)
total_synced += batch_count
logger.info(
f"Synced {total_synced} relationships from {source_database} to {target_database}"
)
return total_synced
def _node_to_sync_dict(
record: neo4j.Record, provider_id: str
) -> tuple[tuple[str, ...], dict[str, Any]]:
"""Transform a source node record into a (grouping_key, sync_dict) pair."""
props = dict(record["props"] or {})
_strip_internal_properties(props)
labels = tuple(sorted(set(record["labels"] or [])))
return labels, {
"provider_element_id": f"{provider_id}:{record['element_id']}",
"props": props,
}
def _rel_to_sync_dict(
record: neo4j.Record, provider_id: str
) -> tuple[str, dict[str, Any]]:
"""Transform a source relationship record into a (grouping_key, sync_dict) pair."""
props = dict(record["props"] or {})
_strip_internal_properties(props)
rel_type = record["rel_type"]
return rel_type, {
"start_element_id": f"{provider_id}:{record['start_element_id']}",
"end_element_id": f"{provider_id}:{record['end_element_id']}",
"provider_element_id": f"{provider_id}:{rel_type}:{record['internal_id']}",
"props": props,
}
def _strip_internal_properties(props: dict[str, Any]) -> None:
"""Remove provider isolation properties before the += spread in sync templates."""
for key in PROVIDER_ISOLATION_PROPERTIES:
@@ -4,7 +4,7 @@ from django.db.models import Count, Q
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding, StatusChoices
from api.models import Finding, Scan, StatusChoices
from prowler.lib.outputs.finding import Finding as FindingOutput
logger = get_task_logger(__name__)
@@ -35,25 +35,26 @@ def _aggregate_requirement_statistics_from_database(
}
"""
requirement_statistics_by_check_id = {}
# TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes
# TODO: review when finding-resource relation changes from 1:1
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Pre-check: skip if the scan's provider is deleted (avoids JOINs in the main query)
if Scan.all_objects.filter(id=scan_id, provider__is_deleted=True).exists():
return requirement_statistics_by_check_id
aggregated_statistics_queryset = (
Finding.all_objects.filter(
tenant_id=tenant_id,
scan_id=scan_id,
muted=False,
resources__provider__is_deleted=False,
)
.values("check_id")
.annotate(
total_findings=Count(
"id",
distinct=True,
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
),
passed_findings=Count(
"id",
distinct=True,
filter=Q(status=StatusChoices.PASS),
),
)
File diff suppressed because it is too large Load Diff
+9 -17
View File
@@ -169,35 +169,27 @@ class TestAggregateRequirementStatistics:
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture):
"""Verify findings without resources are excluded from aggregation."""
def test_skips_aggregation_for_deleted_provider(
self, tenants_fixture, scans_fixture
):
"""Verify aggregation returns empty when the scan's provider is soft-deleted."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
# Finding WITH resource → should be counted
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
# Finding WITHOUT resource → should be EXCLUDED
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
# Soft-delete the provider
provider = scan.provider
provider.is_deleted = True
provider.save(update_fields=["is_deleted"])
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
assert result == {}
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
"""Verify a finding with multiple resources is only counted once."""
@@ -0,0 +1,20 @@
import warnings
from dashboard.common_methods import get_section_containers_rbi
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_rbi(aux, "REQUIREMENTS_ID")
@@ -0,0 +1,20 @@
import warnings
from dashboard.common_methods import get_section_containers_rbi
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_rbi(aux, "REQUIREMENTS_ID")
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
+2
View File
@@ -80,6 +80,8 @@ def load_csv_files(csv_files):
result = result.replace("_M65", " - M65")
if "ALIBABACLOUD" in result:
result = result.replace("_ALIBABACLOUD", " - ALIBABACLOUD")
if "ORACLECLOUD" in result:
result = result.replace("_ORACLECLOUD", " - ORACLECLOUD")
results.append(result)
unique_results = set(results)
@@ -211,7 +211,7 @@ Also is important to keep all code examples as short as possible, including the
| email-security | Ensures detection and protection against phishing, spam, spoofing, etc. |
| forensics-ready | Ensures systems are instrumented to support post-incident investigations. Any digital trace or evidence (logs, volume snapshots, memory dumps, network captures, etc.) preserved immutably and accompanied by integrity guarantees, which can be used in a forensic analysis |
| software-supply-chain | Detects or prevents tampering, unauthorized packages, or third-party risks in software supply chain |
| e3 | M365-specific controls enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) |
| e5 | M365-specific controls enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) |
| e3 | M365 and Azure Entra checks enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) |
| e5 | M365 and Azure Entra checks enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) |
| privilege-escalation | Detects IAM policies or permissions that allow identities to elevate their privileges beyond their intended scope, potentially gaining administrator or higher-level access through specific action combinations |
| ec2-imdsv1 | Identifies EC2 instances using Instance Metadata Service version 1 (IMDSv1), which is vulnerable to SSRF attacks and should be replaced with IMDSv2 for enhanced security |
| ec2-imdsv1 | Identifies EC2 instances using Instance Metadata Service version 1 (IMDSv1), which is vulnerable to SSRF attacks and should be replaced with IMDSv2 for enhanced security |
@@ -121,8 +121,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.20.0"
PROWLER_API_VERSION="5.20.0"
PROWLER_UI_VERSION="5.21.0"
PROWLER_API_VERSION="5.21.0"
```
<Note>
+6
View File
@@ -2,6 +2,12 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.5.0] (Prowler v5.21.0)
### 🚀 Added
- Attack Path tool to get Neo4j DB schema [(#10321)](https://github.com/prowler-cloud/prowler/pull/10321)
## [0.4.0] (Prowler v5.19.0)
### 🚀 Added
+1 -1
View File
@@ -5,7 +5,7 @@ This package provides MCP tools for accessing:
- Prowler Hub: All security artifacts (detections, remediations and frameworks) supported by Prowler
"""
__version__ = "0.4.0"
__version__ = "0.5.0"
__author__ = "Prowler Team"
__email__ = "engineering@prowler.com"
@@ -118,6 +118,51 @@ class AttackPathScansListResponse(BaseModel):
)
class AttackPathCartographySchema(MinimalSerializerMixin, BaseModel):
"""Cartography graph schema metadata for a completed attack paths scan.
Contains the schema URL and provider info needed to fetch the full
Cartography schema markdown for openCypher query generation.
"""
model_config = ConfigDict(frozen=True)
id: str = Field(description="Unique identifier for the schema resource")
provider: str = Field(description="Cloud provider type (aws, azure, gcp, etc.)")
cartography_version: str = Field(description="Version of the Cartography schema")
schema_url: str = Field(description="URL to the Cartography schema page on GitHub")
raw_schema_url: str = Field(
description="Raw URL to fetch the Cartography schema markdown content"
)
schema_content: str | None = Field(
default=None,
description="Full Cartography schema markdown content (populated after fetch)",
)
@classmethod
def from_api_response(
cls, response: dict[str, Any]
) -> "AttackPathCartographySchema":
"""Transform JSON:API schema response to model.
Args:
response: Full API response with data and attributes
Returns:
AttackPathCartographySchema instance
"""
data = response.get("data", {})
attributes = data.get("attributes", {})
return cls(
id=data["id"],
provider=attributes["provider"],
cartography_version=attributes["cartography_version"],
schema_url=attributes["schema_url"],
raw_schema_url=attributes["raw_schema_url"],
)
class AttackPathQueryParameter(MinimalSerializerMixin, BaseModel):
"""Parameter definition for an attack paths query.
@@ -8,6 +8,7 @@ through cloud infrastructure relationships.
from typing import Any, Literal
from prowler_mcp_server.prowler_app.models.attack_paths import (
AttackPathCartographySchema,
AttackPathQuery,
AttackPathQueryResult,
AttackPathScansListResponse,
@@ -225,3 +226,53 @@ class AttackPathsTools(BaseTool):
f"Failed to run attack paths query '{query_id}' on scan {scan_id}: {e}"
)
return {"error": f"Failed to run attack paths query '{query_id}': {str(e)}"}
async def get_attack_paths_cartography_schema(
self,
scan_id: str = Field(
description="UUID of a COMPLETED attack paths scan. Use `prowler_app_list_attack_paths_scans` with state=['completed'] to find scan IDs"
),
) -> dict[str, Any]:
"""Retrieve the Cartography graph schema for a completed attack paths scan.
This tool fetches the full Cartography schema (node labels, relationships,
and properties) so the LLM can write accurate custom openCypher queries
for attack paths analysis.
Two-step flow:
1. Calls the Prowler API to get schema metadata (provider, version, URLs)
2. Fetches the raw Cartography schema markdown from GitHub
Returns:
- id: Schema resource identifier
- provider: Cloud provider type
- cartography_version: Schema version
- schema_url: GitHub page URL for reference
- raw_schema_url: Raw markdown URL
- schema_content: Full Cartography schema markdown with node/relationship definitions
Workflow:
1. Use prowler_app_list_attack_paths_scans to find a completed scan
2. Use this tool to get the schema for the scan's provider
3. Use the schema to craft custom openCypher queries
4. Execute queries with prowler_app_run_attack_paths_query
"""
try:
api_response = await self.api_client.get(
f"/attack-paths-scans/{scan_id}/schema"
)
schema = AttackPathCartographySchema.from_api_response(api_response)
schema_content = await self.api_client.fetch_external_url(
schema.raw_schema_url
)
return schema.model_copy(
update={"schema_content": schema_content}
).model_dump()
except Exception as e:
self.logger.error(
f"Failed to get cartography schema for scan {scan_id}: {e}"
)
return {"error": f"Failed to get cartography schema: {str(e)}"}
@@ -4,11 +4,15 @@ import asyncio
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Dict
from urllib.parse import urlparse
import httpx
from prowler_mcp_server import __version__
from prowler_mcp_server.lib.logger import logger
from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth
ALLOWED_EXTERNAL_DOMAINS: frozenset[str] = frozenset({"raw.githubusercontent.com"})
class HTTPMethod(str, Enum):
"""HTTP methods enum."""
@@ -187,6 +191,47 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
"""
return await self._make_request(HTTPMethod.DELETE, path, params=params)
async def fetch_external_url(self, url: str) -> str:
"""Fetch content from an allowed external URL (unauthenticated).
Uses the existing singleton httpx client with a domain allowlist
to prevent SSRF attacks.
Args:
url: The external URL to fetch content from
Returns:
Raw text content from the URL
Raises:
ValueError: If the URL domain is not in the allowlist
Exception: If the HTTP request fails
"""
parsed = urlparse(url)
if parsed.scheme != "https":
raise ValueError(f"Only HTTPS URLs are allowed, got '{parsed.scheme}'")
if parsed.hostname not in ALLOWED_EXTERNAL_DOMAINS:
raise ValueError(
f"Domain '{parsed.hostname}' is not allowed. "
f"Allowed domains: {', '.join(sorted(ALLOWED_EXTERNAL_DOMAINS))}"
)
try:
response = await self.client.get(
url,
headers={"User-Agent": f"prowler-mcp-server/{__version__}"},
)
response.raise_for_status()
return response.text
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error fetching external URL {url}: {e}")
raise Exception(
f"Failed to fetch external URL: {e.response.status_code}"
) from e
except Exception as e:
logger.error(f"Error fetching external URL {url}: {e}")
raise
async def poll_task_until_complete(
self,
task_id: str,
+1 -1
View File
@@ -11,7 +11,7 @@ description = "MCP server for Prowler ecosystem"
name = "prowler-mcp"
readme = "README.md"
requires-python = ">=3.12"
version = "0.4.0"
version = "0.5.0"
[project.scripts]
prowler-mcp = "prowler_mcp_server.main:main"
+1 -1
View File
@@ -717,7 +717,7 @@ wheels = [
[[package]]
name = "prowler-mcp"
version = "0.3.0"
version = "0.5.0"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
Generated
+13 -33
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -1908,7 +1908,6 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "contextlib2"
@@ -2152,24 +2151,6 @@ files = [
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "deprecated"
version = "1.2.18"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
groups = ["main"]
files = [
{file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"},
{file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"},
]
[package.dependencies]
wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"]
[[package]]
name = "detect-secrets"
version = "1.5.0"
@@ -3156,7 +3137,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.3.6"
jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4"
rpds-py = ">=0.7.1"
@@ -3265,7 +3246,7 @@ files = [
]
[package.dependencies]
certifi = ">=14.5.14"
certifi = ">=14.05.14"
durationpy = ">=0.7"
google-auth = ">=1.0.1"
oauthlib = ">=3.2.2"
@@ -4875,7 +4856,7 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
markers = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@@ -5051,22 +5032,21 @@ files = [
[[package]]
name = "pygithub"
version = "2.5.0"
version = "2.8.0"
description = "Use the full Github API v3"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2"},
{file = "pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf"},
{file = "pygithub-2.8.0-py3-none-any.whl", hash = "sha256:11a3473c1c2f1c39c525d0ee8c559f369c6d46c272cb7321c9b0cabc7aa1ce7d"},
{file = "pygithub-2.8.0.tar.gz", hash = "sha256:72f5f2677d86bc3a8843aa720c6ce4c1c42fb7500243b136e3d5e14ddb5c3386"},
]
[package.dependencies]
Deprecated = "*"
pyjwt = {version = ">=2.4.0", extras = ["crypto"]}
pynacl = ">=1.4.0"
requests = ">=2.14.0"
typing-extensions = ">=4.0.0"
typing-extensions = ">=4.5.0"
urllib3 = ">=1.26.0"
[[package]]
@@ -5118,7 +5098,7 @@ files = [
]
[package.dependencies]
astroid = ">=3.3.8,<=3.4.0.dev0"
astroid = ">=3.3.8,<=3.4.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -5965,10 +5945,10 @@ files = [
]
[package.dependencies]
botocore = ">=1.37.4,<2.0a0"
botocore = ">=1.37.4,<2.0a.0"
[package.extras]
crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
[[package]]
name = "safety"
@@ -6520,7 +6500,7 @@ version = "1.17.2"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
groups = ["dev"]
files = [
{file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"},
{file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"},
@@ -6906,4 +6886,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "386f6cf2bed49290cc4661aa2093ceb018aa6cdaf6864bdfab36f6c2c50e241e"
content-hash = "fa67f98ae1b75ec5a54d1d6a1c33c5412d888ec60cf35fc407606dc48329c0bf"
+45
View File
@@ -2,6 +2,51 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.22.0] (Prowler UNRELEASED)
### 🚀 Added
- `s3_bucket_website_hosting_disabled` check for AWS provider [(#10415)](https://github.com/prowler-cloud/prowler/pull/10415)
---
## [5.21.0] (Prowler v5.21.0)
### 🚀 Added
- `misconfig` scanner as default for Image provider scans [(#10167)](https://github.com/prowler-cloud/prowler/pull/10167)
- `entra_conditional_access_policy_device_code_flow_blocked` check for M365 provider [(#10218)](https://github.com/prowler-cloud/prowler/pull/10218)
- RBI compliance for the Azure provider [(#10339)](https://github.com/prowler-cloud/prowler/pull/10339)
-`entra_conditional_access_policy_require_mfa_for_admin_portals` check for Azure provider and update CIS compliance [(#10330)](https://github.com/prowler-cloud/prowler/pull/10330)
- CheckMetadata Pydantic validators [(#8583)](https://github.com/prowler-cloud/prowler/pull/8583)
- `organization_repository_deletion_limited` check for GitHub provider [(#10185)](https://github.com/prowler-cloud/prowler/pull/10185)
- SecNumCloud 3.2 for the GCP provider [(#10364)](https://github.com/prowler-cloud/prowler/pull/10364)
- SecNumCloud 3.2 for the Azure provider [(#10358)](https://github.com/prowler-cloud/prowler/pull/10358)
- SecNumCloud 3.2 for the Alibaba Cloud provider [(#10370)](https://github.com/prowler-cloud/prowler/pull/10370)
- SecNumCloud 3.2 for the Oracle Cloud provider [(#10371)](https://github.com/prowler-cloud/prowler/pull/10371)
### 🔄 Changed
- Bump `pygithub` from 2.5.0 to 2.8.0 to use native Organization properties
- Update M365 SharePoint service metadata to new format [(#9684)](https://github.com/prowler-cloud/prowler/pull/9684)
- Update M365 Exchange service metadata to new format [(#9683)](https://github.com/prowler-cloud/prowler/pull/9683)
- Update M365 Teams service metadata to new format [(#9685)](https://github.com/prowler-cloud/prowler/pull/9685)
- Update M365 Entra ID service metadata to new format [(#9682)](https://github.com/prowler-cloud/prowler/pull/9682)
- Update ResourceType and Categories for Azure Entra ID service metadata [(#10334)](https://github.com/prowler-cloud/prowler/pull/10334)
- Update OCI Regions to include US DoD regions [(#10375)](https://github.com/prowler-cloud/prowler/pull/10376)
### 🐞 Fixed
- Route53 dangling IP check false positive when using `--region` flag [(#9952)](https://github.com/prowler-cloud/prowler/pull/9952)
- RBI compliance framework support on Prowler Dashboard for the Azure provider [(#10360)](https://github.com/prowler-cloud/prowler/pull/10360)
- CheckMetadata strict validators rejecting valid external tool provider data (image, iac, llm) [(#10363)](https://github.com/prowler-cloud/prowler/pull/10363)
### 🔐 Security
- Bump `multipart` to 1.3.1 to fix [GHSA-p2m9-wcp5-6qw3](https://github.com/defnull/multipart/security/advisories/GHSA-p2m9-wcp5-6qw3) [(#10331)](https://github.com/prowler-cloud/prowler/pull/10331)
---
## [5.20.0] (Prowler v5.20.0)
### 🚀 Added
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -752,7 +752,7 @@
"Id": "2.2.8",
"Description": "Ensure Multi-factor Authentication is Required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{
+1 -1
View File
@@ -1036,7 +1036,7 @@
"Id": "6.2.7",
"Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{
+1 -1
View File
@@ -472,7 +472,7 @@
"Id": "5.2.7",
"Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{
@@ -63,7 +63,7 @@
"Id": "1.1.4",
"Description": "Ensure Multi-factor Authentication is Required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,178 @@
{
"Framework": "RBI-Cyber-Security-Framework",
"Name": "Reserve Bank of India (RBI) Cyber Security Framework",
"Version": "",
"Provider": "GCP",
"Description": "The Reserve Bank had prescribed a set of baseline cyber security controls for primary (Urban) cooperative banks (UCBs) in October 2018. On further examination, it has been decided to prescribe a comprehensive cyber security framework for the UCBs, as a graded approach, based on their digital depth and interconnectedness with the payment systems landscape, digital products offered by them and assessment of cyber security risk. The framework would mandate implementation of progressively stronger security measures based on the nature, variety and scale of digital product offerings of banks.",
"Requirements": [
{
"Id": "annex_i_1_1",
"Name": "Annex I (1.1)",
"Description": "UCBs should maintain an up-to-date business IT Asset Inventory Register containing the following fields, as a minimum: a) Details of the IT Asset (viz., hardware/software/network devices, key personnel, services, etc.), b. Details of systems where customer data are stored, c. Associated business applications, if any, d. Criticality of the IT asset (For example, High/Medium/Low).",
"Attributes": [
{
"ItemId": "annex_i_1_1",
"Service": "gcp"
}
],
"Checks": [
"iam_cloud_asset_inventory_enabled",
"iam_organization_essential_contacts_configured"
]
},
{
"Id": "annex_i_1_3",
"Name": "Annex I (1.3)",
"Description": "Appropriately manage and provide protection within and outside UCB/network, keeping in mind how the data/information is stored, transmitted, processed, accessed and put to use within/outside the UCB's network, and level of risk they are exposed to depending on the sensitivity of the data/information.",
"Attributes": [
{
"ItemId": "annex_i_1_3",
"Service": "gcp"
}
],
"Checks": [
"cloudstorage_bucket_public_access",
"cloudstorage_bucket_uniform_bucket_level_access",
"cloudstorage_bucket_logging_enabled",
"cloudstorage_bucket_versioning_enabled",
"cloudsql_instance_public_access",
"cloudsql_instance_public_ip",
"cloudsql_instance_ssl_connections",
"compute_instance_public_ip",
"compute_instance_encryption_with_csek_enabled",
"compute_image_not_publicly_shared",
"kms_key_rotation_enabled",
"kms_key_not_publicly_accessible",
"bigquery_dataset_public_access",
"bigquery_dataset_cmk_encryption"
]
},
{
"Id": "annex_i_5_1",
"Name": "Annex I (5.1)",
"Description": "The firewall configurations should be set to the highest security level and evaluation of critical device (such as firewall, network switches, security devices, etc.) configurations should be done periodically.",
"Attributes": [
{
"ItemId": "annex_i_5_1",
"Service": "compute"
}
],
"Checks": [
"compute_firewall_rdp_access_from_the_internet_allowed",
"compute_firewall_ssh_access_from_the_internet_allowed",
"compute_network_not_legacy",
"compute_network_default_in_use",
"compute_subnet_flow_logs_enabled",
"compute_network_dns_logging_enabled",
"dns_dnssec_disabled"
]
},
{
"Id": "annex_i_6",
"Name": "Annex I (6)",
"Description": "Put in place systems and processes to identify, track, manage and monitor the status of patches to servers, operating system and application software running at the systems used by the UCB officials (end-users). Implement and update antivirus protection for all servers and applicable end points preferably through a centralised system.",
"Attributes": [
{
"ItemId": "annex_i_6",
"Service": "gcp"
}
],
"Checks": [
"compute_instance_shielded_vm_enabled",
"compute_project_os_login_enabled",
"artifacts_container_analysis_enabled",
"gcr_container_scanning_enabled"
]
},
{
"Id": "annex_i_7_1",
"Name": "Annex I (7.1)",
"Description": "Disallow administrative rights on end-user workstations/PCs/laptops and provide access rights on a 'need to know' and 'need to do' basis.",
"Attributes": [
{
"ItemId": "annex_i_7_1",
"Service": "iam"
}
],
"Checks": [
"iam_sa_no_administrative_privileges",
"iam_no_service_roles_at_project_level",
"iam_role_sa_enforce_separation_of_duties",
"iam_role_kms_enforce_separation_of_duties",
"iam_sa_no_user_managed_keys",
"compute_instance_default_service_account_in_use",
"compute_instance_default_service_account_in_use_with_full_api_access"
]
},
{
"Id": "annex_i_7_2",
"Name": "Annex I (7.2)",
"Description": "Passwords should be set as complex and lengthy and users should not use same passwords for all the applications/systems/devices.",
"Attributes": [
{
"ItemId": "annex_i_7_2",
"Service": "iam"
}
],
"Checks": [
"compute_project_os_login_2fa_enabled",
"iam_account_access_approval_enabled"
]
},
{
"Id": "annex_i_7_3",
"Name": "Annex I (7.3)",
"Description": "Remote Desktop Protocol (RDP) which allows others to access the computer remotely over a network or over the internet should be always disabled and should be enabled only with the approval of the authorised officer of the UCB. Logs for such remote access shall be enabled and monitored for suspicious activities.",
"Attributes": [
{
"ItemId": "annex_i_7_3",
"Service": "compute"
}
],
"Checks": [
"compute_firewall_rdp_access_from_the_internet_allowed",
"compute_firewall_ssh_access_from_the_internet_allowed",
"compute_project_os_login_enabled"
]
},
{
"Id": "annex_i_7_4",
"Name": "Annex I (7.4)",
"Description": "Implement appropriate (e.g. centralised) systems and controls to allow, manage, log and monitor privileged/super user/administrative access to critical systems (servers/databases, applications, network devices etc.)",
"Attributes": [
{
"ItemId": "annex_i_7_4",
"Service": "logging"
}
],
"Checks": [
"iam_audit_logs_enabled",
"logging_sink_created",
"cloudstorage_audit_logs_enabled",
"compute_loadbalancer_logging_enabled",
"compute_subnet_flow_logs_enabled",
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled"
]
},
{
"Id": "annex_i_12",
"Name": "Annex I (12)",
"Description": "Take periodic back up of the important data and store this data 'off line' (i.e., transferring important files to a storage device that can be detached from a computer/system after copying all the files).",
"Attributes": [
{
"ItemId": "annex_i_12",
"Service": "gcp"
}
],
"Checks": [
"cloudsql_instance_automated_backups",
"cloudstorage_bucket_versioning_enabled",
"cloudstorage_bucket_soft_delete_enabled",
"cloudstorage_bucket_lifecycle_management_enabled",
"compute_snapshot_not_outdated"
]
}
]
}
File diff suppressed because it is too large Load Diff
@@ -499,7 +499,9 @@
{
"Id": "1.2.3",
"Description": "Ensure only a limited number of trusted users can delete repositories.",
"Checks": [],
"Checks": [
"organization_repository_deletion_limited"
],
"Attributes": [
{
"Section": "1 Source Code",
+3 -1
View File
@@ -1606,7 +1606,9 @@
{
"Id": "5.2.2.12",
"Description": "The Microsoft identity platform supports the device authorization grant, which allows users to sign in to input-constrained devices such as a smart TV, IoT device, or a printer. Ensure the device code sign-in flow is blocked.",
"Checks": [],
"Checks": [
"entra_conditional_access_policy_device_code_flow_blocked"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -243,6 +243,7 @@
"entra_break_glass_account_fido2_security_key_registered",
"entra_default_app_management_policy_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -464,6 +465,7 @@
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled",
"entra_legacy_authentication_blocked",
@@ -694,8 +696,9 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_break_glass_account_fido2_security_key_registered",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_identity_protection_sign_in_risk_enabled",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
File diff suppressed because it is too large Load Diff

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