Compare commits

...
Author SHA1 Message Date
Hugo P.Brito 210fd341e1 p.s: it was an absolute pleasure 2026-09-02 15:15:30 +01:00
f013a5e1ad chore(changelog): v5.41.0 highlights (#12709)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-09-02 13:51:45 +02:00
Prowler Botandprowler-bot 18453e592e chore(changelog): v5.41.0 (#12707)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-09-02 11:23:18 +02:00
Jonathan Nguyen 86e4408f29 feat(ecr): assess enhanced scanning on registries holding repositories (#12660) 2026-09-02 08:53:52 +02:00
Jonathan Nguyen ae43d21efb fix(sagemaker): read DirectInternetAccess instead of RootAccess on notebook instances (#12659) 2026-09-01 18:22:41 +02:00
Pedro Martín 7c84822fa3 fix(image): skip non-image OCI artifacts in registry scan (#12695) 2026-09-01 17:18:47 +02:00
Daniel Barranquero 51c5fa7168 fix(checks): report MANUAL instead of FAIL on permission and data-availability errors (#12645) 2026-09-01 17:16:56 +02:00
Jonathan Nguyen e9121f5f1a feat(cloudwatch): add agentcore log group data protection policy check (#12662) 2026-09-01 17:04:16 +02:00
Jonathan Nguyen 821fe43efd feat(iam): scope AgentCore PassRole and workload token grants, and flag unbound service trust (#12664) 2026-09-01 17:02:05 +02:00
Jonathan Nguyen 9ffbb4b758 fix(ecr): read each registry scanning rule's frequency instead of assuming scan on push (#12560) 2026-09-01 16:53:58 +02:00
Jonathan Nguyen 9c5285adc3 fix(cloudwatch): skip metric filters whose log group was not retrieved (#12561) 2026-09-01 14:00:41 +02:00
Pedro Martín f295d290dd feat(image): private network allowlist for SSRF guard (#12678) 2026-09-01 14:00:04 +02:00
Jonathan Nguyen e5df95c259 feat(eks): assess Kubernetes network policy enforcement in the Amazon VPC CNI add-on (#12661) 2026-09-01 13:40:45 +02:00
Jonathan Nguyen b6a8af3c54 feat(guardduty): assess unified Runtime Monitoring and AI Protection (#12564) 2026-09-01 13:18:57 +02:00
Pedro MartínandLydia Vilchez fb7064401b feat(compliance): add CIS 1.4 google workspace compliance (#12513)
Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
2026-09-01 09:35:39 +02:00
Josema Camacho 8d60f9703a fix(api): limit mute rules to current and future scans (#12681) 2026-09-01 09:33:15 +02:00
Pablo Fernandez Guerra (PFE) ceb601028e fix(ui): apply Slack integration design feedback (#12677) 2026-08-31 18:27:11 +02:00
Pedro MartínandDavid 6422178b76 feat(sdk): AWS partition selection via PROWLER_AWS_PARTITION (#12680)
Co-authored-by: David <david.copo@gmail.com>
2026-08-31 18:13:30 +02:00
Daniel BarranqueroandJosema Camacho 587c47bfe2 feat(api): add finding labels, finding URL and tenant info to Jira issues (#12540)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-08-31 18:00:46 +02:00
Rubén De la Torre VicoandClaude Opus 5 13a31d9225 feat(mcp): raise instead of returning error objects in the Prowler Docs tools (#12534)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:36:19 +02:00
Pablo Fernandez Guerra (PFE)andalejandrobailo 0715619435 feat(ui): import Prowler OCSF findings from the Scans page (#12554)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-08-31 17:14:12 +02:00
Rubén De la Torre Vico 1679094f22 feat(mcp): raise instead of returning error objects in the Prowler Hub tools (#12533) 2026-08-31 13:09:34 +02:00
Rubén De la Torre Vico f05a490cd7 feat(mcp): raise instead of returning error objects in the Prowler App tools (#12532) 2026-08-31 10:57:58 +02:00
Alejandro Bailo 89988dada5 fix(ui): refresh cached permissions after token rotation (#12640) 2026-08-31 10:49:10 +02:00
ye11oc4tandDaniel Barranquero 0326844527 fix(github): paginate repository discovery (#12460)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-31 10:15:29 +02:00
mintlify[bot] 73d5c6952b docs: fix typos and grammar (#12672)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-08-31 09:18:08 +02:00
mintlify[bot] ad76b3026f docs: brand tone and writing style fixes (#12671)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-08-31 09:15:46 +02:00
Utkarsh Batham e21946874f feat(memorydb): add memorydb_cluster_in_transit_encryption_enabled check (#12246) 2026-08-28 14:03:10 +02:00
SejalandDaniel Barranquero 2cae2058e9 feat(aws): add elasticbeanstalk_environment_no_secrets_in_configuration check (#12378)
Signed-off-by: unknown <sej1306kook@gmail.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-28 13:54:23 +02:00
Daniel BarranqueroandJosema Camacho c88f745038 feat(jira): return the created issue, sanitize labels and add bulk status lookup (#12539)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-08-28 13:47:12 +02:00
Prowler Botandprowler-bot c923c58a39 chore(release): Bump versions to v5.41.0 (#12647)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-28 13:16:00 +02:00
c3bee8c21e chore(changelog): v5.40.0 highlights (#12569)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2026-08-28 13:10:06 +02:00
Prowler Botandprowler-bot 4d13e8432e chore(changelog): v5.40.0 (#12642)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-28 11:51:30 +02:00
Pedro Martín 5f24bec9fe fix(compliance): correct Cyber Essentials mappings and remediation text (#12596) 2026-08-28 11:08:15 +02:00
César Arroba afefb8f333 fix(api): apply findings partition max age in months, not days (#12580) 2026-08-28 10:34:12 +02:00
Rubén De la Torre Vico 2b81fdcc04 fix(mcp): call the Mintlify search endpoint the documentation moved to (#12578) 2026-08-27 16:48:38 +02:00
Pablo Fernandez Guerra (PFE) db298c1d48 fix(ui): redirect the Slack OAuth callback relative to the browser's origin (#12577) 2026-08-27 16:13:36 +02:00
2877c3d6c0 fix(slack): handle scans that produce no findings (#12229)
Co-authored-by: Juhef <117518034+juheff@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-27 14:03:33 +02:00
ee64c17108 fix(kubernetes): return empty list when resource gather fails (#12225)
Co-authored-by: Juhef <117518034+juheff@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-27 13:55:46 +02:00
Pablo Fernandez Guerra (PFE)andalejandrobailo a610314eba fix(ui): complete Slack OAuth callback server-side to avoid router race (#12572)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-08-27 13:54:15 +02:00
Muhammad Ibrahimandpedrooot 301edea7ce feat(compliance): add Cyber Essentials 3.3 for Azure (#11588)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2026-08-27 13:31:18 +02:00
c89d900aae fix(iac): raise typed exceptions instead of sys.exit on provider failures (#12227)
Co-authored-by: Juhef <117518034+juheff@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-27 12:15:50 +02:00
Hugo Pereira Brito 4cfb4eeb96 fix(sdk): use system trust store for push-to-cloud (#12485) 2026-08-27 11:07:14 +01:00
Pedro Martínandalejandrobailo f19478f2f6 fix(compliance): discover universal frameworks from entry point (#12536)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-08-27 09:17:39 +02:00
Chethas DileepandDaniel Barranquero 2f11b16299 feat(github): add repository_default_workflow_permissions_read_only check (#12143)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-27 09:00:42 +02:00
Pablo Fernandez Guerra (PFE) 654d2c9f17 docs: document Slack channel destinations for alerts (#12496) 2026-08-27 08:53:07 +02:00
Chethas DileepandDaniel Barranquero 2721d42594 feat(github): add organization_actions_pull_request_approval_disabled check (#12394)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-27 08:25:32 +02:00
Daniel Barranquero bd1956446d fix(alibabacloud): read OSS bucket sub-resource configs via the SDK execute path (#12546) 2026-08-26 16:41:13 +02:00
Pablo Fernandez Guerra (PFE) d26d7cf91a feat(ui): show a rule's destinations in the alerts list (#12493) 2026-08-26 13:36:31 +02:00
Pablo Fernandez Guerra (PFE) 4c20bf1fac feat(ui): let alert rules target authorized Slack channels (#12492) 2026-08-26 12:55:49 +02:00
Chethas DileepandDaniel Barranquero 9dc53ffc60 feat(github): add organization_default_workflow_permissions_read_only check (#12122)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-26 12:21:49 +02:00
Alan Buscagliaandalejandrobailo 95642fb220 feat(ui): add Lighthouse request outcome feedback (#12419)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-08-26 11:34:40 +02:00
Pedro Martín 6449f3a592 fix(container): patch the high OpenSSL CVEs for container img (#12549) 2026-08-26 11:10:55 +02:00
Rubén De la Torre Vico 91e6cb798d feat(mcp): classify shared tool failures and stop relaying upstream bodies (#12531) 2026-08-26 11:06:07 +02:00
Rubén De la Torre Vico bcd37988b5 fix(mcp): patch the high openssl CVE in the container image (#12547) 2026-08-26 10:45:41 +02:00
Alejandro Bailo 7a64e1a1d1 feat(ui): add cancelled subscription variant to the trial sidebar banner (#12538) 2026-08-26 08:59:00 +02:00
Pablo Fernandez Guerra (PFE) 39c85ffb77 feat(ui): authorize multiple Slack destination channels (#12491) 2026-08-26 08:52:56 +02:00
Alex ChenandDaniel Barranquero 301ca50541 feat(alibabacloud): add oss_bucket_server_side_encryption_enabled check (#11981)
Signed-off-by: Alex Chen <l46983284@gmail.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-25 17:41:52 +02:00
Rubén De la Torre Vico f299e1d9ac fix(mcp): patch the two high sqlite CVEs in the container image (#12537) 2026-08-25 17:41:44 +02:00
João Mesquitaandpedrooot 465e35bf54 fix(compliance): correct AWS FSBP check mapping for IAM.9 and EKS.1 (#12372)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2026-08-25 17:38:33 +02:00
Pablo Fernandez Guerra (PFE) 829af2e3f7 docs: add Slack integration guide (#12438) 2026-08-25 15:44:34 +02:00
Hugo Pereira Brito 51c65bdc69 docs: restructure organization documentation (#12521) 2026-08-25 12:49:23 +01:00
Hugo Pereira Brito d0c088dd0a docs: explain AWS Organization account membership updates (#12512) 2026-08-25 12:47:23 +01:00
abidedavanaandDaniel Barranquero 411d112165 feat(alibabacloud): add oss_bucket_versioning_enabled check (#11913)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-25 13:03:25 +02:00
Gabrielandpedrooot 8a4cc8780d feat(kubernetes): include cluster name in compliance reports (#12506)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2026-08-25 11:08:04 +02:00
Pepe Fagoaga cd4d2a27e3 chore(lighthouse): rename not tested to not connected (#12523) 2026-08-25 09:21:18 +02:00
9898cf7364 feat(m365): add defender_domain_dmarc_records_published check (#11936)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: hdy2001 <56308320+hdy2001@users.noreply.github.com>
2026-08-24 16:49:36 +02:00
83d8cfa829 fix(aws): treat security groups on Batch compute environments as used (#12458)
Co-authored-by: hackertwinten <193916571+hackertwinten@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-24 16:18:56 +02:00
mintlify[bot] 5202a68cf0 docs: brand tone and writing style fixes (#12510)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-08-24 11:17:37 +02:00
Hugo Pereira Brito e3a3acc799 fix(api): upgrade sqlparse to 0.6.0 (#12509) 2026-08-24 08:47:29 +02:00
Pablo Fernandez Guerra (PFE) 3e000faa31 feat(ui): add Slack disconnect and revoked-credential recovery (#12437) 2026-08-21 12:42:47 +02:00
Jonathan NguyenandHugo P.Brito f39c92b8f8 feat(bedrock): add model artifact and guardrail grounding security checks for the AWS provider (#12459)
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-08-21 10:50:34 +01:00
Pablo Fernandez Guerra (PFE) 823efc5ab1 feat(ui): pick a Slack channel and verify it (#12436) 2026-08-21 11:45:24 +02:00
Pablo Fernandez Guerra (PFE) 75d7fa5006 feat(ui): add Slack integration connect flow (#12435) 2026-08-21 10:49:46 +02:00
Prowler Botandprowler-bot db25484ccb feat(aws): Update regions for AWS services (#12472)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-20 16:02:56 +01:00
Johannes EnglerandHugo P.Brito 3da4209ee7 feat(stackit): add ske_cluster_no_public_endpoint check (#11943)
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-08-20 13:01:01 +01:00
acb6ff0425 feat(providers/huaweicloud): add vpc_security_group_open_egress check (#12209)
Co-authored-by: tomitobio <tomitobio@users.noreply.github.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-08-20 10:52:13 +01:00
Pablo Fernandez Guerra (PFE) ba564af4f4 fix(ci): unblock the Python runtime download in blocked-egress jobs (#12490) 2026-08-20 10:48:16 +02:00
Eugene C.andHugo P.Brito 0b9791ffdc feat(ecr): add ecr_repository_image_no_secrets check (#12123)
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-08-18 11:10:07 +01:00
Prowler Botandprowler-bot f6defefb58 chore(changelog): v5.39.1 forward-sync to master (#12483)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-18 11:32:20 +02:00
ye11oc4tandHugo P.Brito f3224d0988 fix(ses): evaluate all identity authorization policies (#12464)
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-08-17 14:19:45 +01:00
Pepe Fagoaga 450e6ba553 chore(api): drop temporary SDK pin overrides after cryptography cap bump (#12473) 2026-08-17 13:42:09 +02:00
2cd93fe119 fix(sdk): skip undescribed ECS task definitions (#12217)
Co-authored-by: Nguyễn Công Thuận Huy <nguyencongthuanhuy@gmail.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-08-17 12:42:06 +01:00
Pablo Fernandez Guerra (PFE) f807b22ea6 ci: lint .github markdown and fix the violations it exposed (#12290) 2026-08-17 13:00:32 +02:00
Pepe Fagoaga b6e9967da6 fix(deps): make published wheels installable and add package checks (#12467) 2026-08-17 12:34:11 +02:00
Hugo Pereira Brito 16e62f7514 ci(labeler): cover existing provider labels (#12476) 2026-08-17 11:33:22 +01:00
Adrián Peña 13ce9436b3 chore: update Trivy to 0.74.0 (#12466) 2026-08-17 10:25:28 +02:00
mintlify[bot] d3ced63397 fix(docs): typos and grammar (#12468)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-08-17 09:21:56 +02:00
Adrián Peña 758b696ca5 feat(ui): highlight imported providers (#12447) 2026-08-17 08:53:14 +02:00
Pepe Fagoaga 0d3ce45374 fix(pypi): bump to pypa/gh-action-pypi-publish v1.14.2 (#12456) 2026-08-14 14:57:07 +02:00
Alejandro Bailo f35666ff0a fix(ui): settle scan auto-refresh safely (#12455) 2026-08-14 11:48:08 +02:00
Pedro Martín 0758c3585d feat(rolesanywhere): flag profiles with unscoped sessions (#12416) 2026-08-13 17:06:24 +02:00
dd882c70e7 chore(release): Bump versions to v5.40.0 (#12443)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-08-13 13:49:26 +02:00
Hugo Pereira BritoandPablo F.G d05c9fbb31 feat(ui): add trial usage sidebar banner (#12420)
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
2026-08-13 11:36:59 +01:00
Josema Camacho 7bde42ffb9 docs: update attack paths documentation for grouped graphs (#12440) 2026-08-13 12:25:14 +02:00
Pepe Fagoaga 0d3df0fd0b chore(changelog): v5.39.0 release highlights (#12432) 2026-08-13 12:08:15 +02:00
Pedro Martín ab996417e6 fix(ci): bump Trivy to v0.73.0 to fix CVE-2026-46600 (#12444) 2026-08-13 12:04:51 +02:00
644 changed files with 61824 additions and 5069 deletions
+1 -1
View File
@@ -158,7 +158,7 @@ SENTRY_RELEASE=local
# REO_DEV_CLIENT_ID=
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.39.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.41.0
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
+2 -2
View File
@@ -64,7 +64,7 @@ runs:
scanners: 'vuln'
ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate
timeout: '5m'
version: 'v0.72.0'
version: 'v0.74.0'
# Not trivyignores: that input drops the .yaml extension Trivy parses by.
env:
TRIVY_IGNOREFILE: '.trivyignore.yaml'
@@ -81,7 +81,7 @@ runs:
scanners: 'vuln'
ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate
timeout: '5m'
version: 'v0.72.0'
version: 'v0.74.0'
# Not trivyignores: that input drops the .yaml extension Trivy parses by.
env:
TRIVY_IGNOREFILE: '.trivyignore.yaml'
+6 -6
View File
@@ -199,7 +199,7 @@ You MUST structure your response using this EXACT format. Do NOT include anythin
### For Check Logic Bug
```
```markdown
### AI Assessment [Experimental]: Check Logic Bug
**Component**: {component from issue template}
@@ -297,7 +297,7 @@ Write tests FIRST (TDD). The skills contain all testing conventions and patterns
### For Bug (non-check)
```
```markdown
### AI Assessment [Experimental]: Bug
**Component**: {CLI/SDK | API | UI | Dashboard | MCP Server | Other}
@@ -378,7 +378,7 @@ Write tests FIRST (TDD). The skills contain all testing conventions and patterns
### For Already Fixed
```
```markdown
### AI Assessment [Experimental]: Already Fixed
**Component**: {component}
@@ -401,7 +401,7 @@ Upgrade to the latest version. Close the issue as resolved.
### For Feature Request
```
```markdown
### AI Assessment [Experimental]: Feature Request
**Component**: {component}
@@ -419,7 +419,7 @@ Upgrade to the latest version. Close the issue as resolved.
### For Not a Bug
```
```markdown
### AI Assessment [Experimental]: Not a Bug
**Component**: {component}
@@ -440,7 +440,7 @@ Upgrade to the latest version. Close the issue as resolved.
### For Needs More Information
```
```markdown
### AI Assessment [Experimental]: Needs More Information
**Component**: {component or "Unknown"}
+15
View File
@@ -52,6 +52,16 @@ provider/alibabacloud:
- any-glob-to-any-file: "prowler/providers/alibabacloud/**"
- any-glob-to-any-file: "tests/providers/alibabacloud/**"
provider/huaweicloud:
- changed-files:
- any-glob-to-any-file: "prowler/providers/huaweicloud/**"
- any-glob-to-any-file: "tests/providers/huaweicloud/**"
provider/image:
- changed-files:
- any-glob-to-any-file: "prowler/providers/image/**"
- any-glob-to-any-file: "tests/providers/image/**"
provider/cloudflare:
- changed-files:
- any-glob-to-any-file: "prowler/providers/cloudflare/**"
@@ -82,6 +92,11 @@ provider/linode:
- any-glob-to-any-file: "prowler/providers/linode/**"
- any-glob-to-any-file: "tests/providers/linode/**"
provider/stackit:
- changed-files:
- any-glob-to-any-file: "prowler/providers/stackit/**"
- any-glob-to-any-file: "tests/providers/stackit/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
+5 -5
View File
@@ -8,11 +8,11 @@ These JSON templates are used with the `slackapi/slack-github-action` using the
### Available Templates
**Container Releases**
#### Container Releases
- `container-release-started.json`: Simple one-line notification when container push starts
- `container-release-completed.json`: Simple one-line notification when container release completes
**Deployments**
#### Deployments
- `deployment-started.json`: Deployment start notification with Block Kit formatting
- `deployment-completed.json`: Deployment completion notification (updates the start message)
@@ -416,17 +416,17 @@ For deployments that start with one message and update it with the final status:
### Container Release (Simple One-Line)
**Start message:**
```
```text
API container release 4.5.0 push started... View run
```
**Completion message (success):**
```
```text
[✓] API container release 4.5.0 push completed successfully! View run
```
**Completion message (failure):**
```
```text
[✗] API container release 4.5.0 push failed View run
```
+1
View File
@@ -61,6 +61,7 @@ jobs:
api.github.com:443
github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
# "andoniaf"
"cesararroba"
"danibarranqueroo"
"HugoPBrito"
# "HugoPBrito"
"jfagoagas"
"josema-xyz"
"lydiavilchez"
+5 -1
View File
@@ -55,6 +55,10 @@ jobs:
# Pin must match .pre-commit-config.yaml so prek and CI behave identically.
# pnpm dlx doesn't accept --ignore-scripts as a flag; the env var
# disables postinstall scripts on transitives the same way.
#
# Files come from `git ls-files` because markdownlint doesn't traverse
# dot-directories, so `.github/**/*.md` went unlinted.
# `.markdownlintignore` still applies to the listed paths.
env:
pnpm_config_ignore_scripts: 'true'
run: pnpm dlx markdownlint-cli@0.45.0 '**/*.md'
run: git ls-files -z '*.md' | xargs -0 -r pnpm dlx markdownlint-cli@0.45.0 --
@@ -114,6 +114,8 @@ jobs:
egress-policy: block
allowed-endpoints: >
auth.docker.io:443
dl-cdn.alpinelinux.org:443
dualstack.j.sni.global.fastly.net:443
files.pythonhosted.org:443
ghcr.io:443
github.com:443
@@ -81,6 +81,8 @@ jobs:
pkg-containers.githubusercontent.com:443
files.pythonhosted.org:443
pypi.org:443
dl-cdn.alpinelinux.org:443
dualstack.j.sni.global.fastly.net:443
api.github.com:443
mirror.gcr.io:443
check.trivy.dev:443
+1 -1
View File
@@ -113,7 +113,7 @@ jobs:
- name: Publish prowler-mcp package to PyPI
if: steps.pypi-check.outputs.skip != 'true'
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: ${{ env.WORKING_DIRECTORY }}/dist/
print-hash: true
+1
View File
@@ -35,6 +35,7 @@ jobs:
api.github.com:443
github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
+196
View File
@@ -0,0 +1,196 @@
name: 'SDK: Package Checks'
# Rehearses the PyPI release on every packaging change and once a week, from the
# consumer's side. Two incidents this guards against:
#
# - 5.38.0 shipped an unsatisfiable Requires-Dist (cryptography==50.0.0 while
# alibabacloud-tea-openapi and pyopenssl cap it below 49). A [tool.uv] override hid
# the conflict inside the repo; pip could not install the wheel and silently
# resolved `pip install prowler` to 5.37.1 for a week.
# - 5.39.0 never published: an unpinned build backend started emitting core metadata
# 2.5 and the twine bundled in the publish action rejected it.
#
# Both were only detectable at release time because nothing built and installed the
# artifact earlier. The weekly run also catches releases yanked from PyPI after we
# pinned them (zstd 1.5.7.3, "buggy - not thread safe", sat in uv.lock for months).
on:
push:
branches:
- 'master'
- 'v5.*'
pull_request:
branches:
- 'master'
- 'v5.*'
schedule:
# Monday 06:00 UTC. Yanks and upstream releases happen without a commit here.
- cron: '0 6 * * 1'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
env:
# Must equal the twine bundled in the pypa/gh-action-pypi-publish pin used by
# sdk-pypi-release.yml (requirements/runtime.txt in that repo at the pinned tag).
# A metadata check that passes here must pass there.
TWINE_VERSION: '7.0.0'
jobs:
changes:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
# Scheduled and manual runs always execute; pushes and PRs only when a packaging
# input changed. Jobs skipped this way still report success to branch protection.
run: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || steps.filter.outputs.any_changed == 'true' }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: block
allowed-endpoints: >
github.com:443
api.github.com:443
- name: Checkout repository
if: github.event_name == 'push' || github.event_name == 'pull_request'
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: Detect packaging changes
if: github.event_name == 'push' || github.event_name == 'pull_request'
id: filter
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
pyproject.toml
uv.lock
README.md
util/replicate_pypi_package.py
util/check_yanked_pins.py
api/pyproject.toml
api/uv.lock
mcp_server/pyproject.toml
mcp_server/uv.lock
.github/workflows/sdk-package-checks.yml
.github/workflows/sdk-pypi-release.yml
.github/actions/setup-python-uv/**
install-from-wheel:
needs: changes
if: needs.changes.outputs.run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python-version:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
package:
- 'prowler'
include:
# prowler-cloud is the same tree renamed by util/replicate_pypi_package.py;
# one Python is enough to prove the rename and its build still work.
- python-version: '3.12'
package: 'prowler-cloud'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: block
allowed-endpoints: >
github.com:443
api.github.com:443
release-assets.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Python with uv
uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ matrix.python-version }}
install-dependencies: 'false'
- name: Rename package to prowler-cloud
if: matrix.package == 'prowler-cloud'
run: |
pip install --no-cache-dir toml
python util/replicate_pypi_package.py
- name: Build sdist and wheel
run: uv build
- name: Check metadata with the release workflow's twine
run: uvx --from "twine==${TWINE_VERSION}" twine check --strict dist/*
- name: Install the wheel with pip into a clean virtualenv
# Plain pip, --isolated, from outside the repo: consumers never see [tool.uv]
# override-dependencies or constraint-dependencies, so neither does this step.
run: |
python -m venv "${RUNNER_TEMP}/consumer"
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir "${GITHUB_WORKSPACE}"/dist/*.whl
- name: Smoke test the installed CLI
run: |
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/consumer/bin/prowler" --version
# Loads every AWS check module from the installed wheel: catches files missing
# from the package. grep fails the step if the summary line never appears.
"${RUNNER_TEMP}/consumer/bin/prowler" aws --list-checks | grep 'available checks'
pinned-releases-not-yanked:
needs: changes
if: needs.changes.outputs.run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: block
allowed-endpoints: >
github.com:443
api.github.com:443
release-assets.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.12'
- name: Check every pinned and locked release against PyPI
run: python util/check_yanked_pins.py . api mcp_server
+22 -2
View File
@@ -84,8 +84,18 @@ jobs:
- name: Build Prowler package
run: uv build
- name: Verify the wheel installs with pip
# Same check as "SDK: Package Checks", repeated on the exact artifact about to be
# published. Plain pip, --isolated, from outside the repo: an unsatisfiable
# Requires-Dist fails here instead of on users' machines (5.38.0 shipped one).
run: |
python -m venv "${RUNNER_TEMP}/consumer"
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir --dry-run "${GITHUB_WORKSPACE}"/dist/*.whl
- name: Publish Prowler package to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
print-hash: true
@@ -128,7 +138,17 @@ jobs:
- name: Build prowler-cloud package
run: uv build
- name: Verify the wheel installs with pip
# Same check as "SDK: Package Checks", repeated on the exact artifact about to be
# published. Plain pip, --isolated, from outside the repo: an unsatisfiable
# Requires-Dist fails here instead of on users' machines (5.38.0 shipped one).
run: |
python -m venv "${RUNNER_TEMP}/consumer"
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir --dry-run "${GITHUB_WORKSPACE}"/dist/*.whl
- name: Publish prowler-cloud package to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
print-hash: true
@@ -57,6 +57,7 @@ jobs:
egress-policy: block
allowed-endpoints: >
github.com:443
release-assets.githubusercontent.com:443
pypi.org:443
files.pythonhosted.org:443
+2 -6
View File
@@ -135,8 +135,8 @@ vulnerabilities:
# Modules compiled into the Trivy binary the images ship. The binary is pinned by version
# and verified by checksum in the Dockerfile; only a rebuild by its vendor moves these.
# CVE-2026-71556 affects go-git worktree operations that can follow symlinks outside a
# cloned repository. Trivy 0.72.0 contains go-git 5.19.1, and even the latest published
# Trivy release, 0.73.0, still pins that vulnerable version:
# cloned repository. Trivy 0.73.0, the latest published release and the version the
# images ship, still pins that vulnerable version:
# https://github.com/aquasecurity/trivy/blob/v0.73.0/go.mod#L46
# Trivy main already contains the 5.19.2 fix, but no published release includes it yet:
# https://github.com/aquasecurity/trivy/commit/a2edba9a03987ba0d2ebc8212c1a9a1e6979497b
@@ -164,7 +164,3 @@ vulnerabilities:
purls:
- "pkg:golang/oras.land/oras-go/v2"
expired_at: 2026-12-31
- id: CVE-2026-39822
purls:
- "pkg:golang/stdlib"
expired_at: 2026-12-31
+13 -3
View File
@@ -8,24 +8,34 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.72.0
ARG TRIVY_VERSION=0.74.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
ARG TRIVY_SHA256_AMD64=bbb64b9695866ce4a7a8f5c9592002c5961cab378577fa3f8a040df362b9b2ea
ARG TRIVY_SHA256_ARM64=2ca2c023109c2db6b2b77366b6717291452d4531167377d95c79547f0c8e3467
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0
ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
# High CVEs fixed in Debian trixie-security but not yet in the pinned base image:
# openssl/libssl3t64/openssl-provider-legacy 3.5.7-1~deb13u2 CVE-2026-14456,
# -14457, -18798, -54874, -63072, -63073, -63074, -63075, -63076, -75803
# (image ships 3.5.6-1~deb13u2)
# Taken as a targeted --only-upgrade rather than by moving the digest: the newest
# published python:3.12-slim-trixie carries the same vulnerable version. The three
# packages are all built from openssl and are flagged separately, so all are named.
# Drop them once the base image ships 3.5.7-1~deb13u2 or later.
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
wget libicu76 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \
build-essential pkg-config libzstd-dev zlib1g-dev \
&& apt-get install -y --no-install-recommends --only-upgrade \
util-linux libssl3t64 openssl openssl-provider-legacy \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
+1 -1
View File
@@ -139,7 +139,7 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
| MongoDB Atlas | 10 | 3 | 1 | 8 | Official | UI, API, CLI |
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
| Image | N/A | N/A | N/A | N/A | Official | UI, API, CLI |
| Google Workspace | 65 | 11 | 3 | 6 | Official | UI, API, CLI |
| Google Workspace | 65 | 11 | 4 | 6 | Official | UI, API, CLI |
| OpenStack | 34 | 5 | 1 | 9 | Official | UI, API, CLI |
| Vercel | 26 | 6 | 1 | 8 | Official | UI, API, CLI |
| Okta | 29 | 8 | 2 | 2 | Official | UI, API, CLI |
+4
View File
@@ -59,5 +59,9 @@ DJANGO_GITHUB_OAUTH_CLIENT_ID=""
DJANGO_GITHUB_OAUTH_CLIENT_SECRET=""
DJANGO_GITHUB_OAUTH_CALLBACK_URL=""
# Public base URL of the Prowler UI, used to link Jira issues back to findings.
# Leave empty to omit the link.
DJANGO_UI_BASE_URL=""
# Deletion Task Batch Size
DJANGO_DELETION_BATCH_SIZE=5000
+42
View File
@@ -4,6 +4,48 @@ All notable changes to the **Prowler API** are documented in this file.
<!-- changelog: release notes start -->
## [1.42.0] (Prowler v5.41.0)
### 🚀 Added
- Jira issues created from Prowler Cloud now carry the `prowler`, `prowler-<provider>`, `prowler-<severity>`, `prowler-<check-id>`, and `prowler-finding-<finding-uid>` labels, a link back to the finding when `DJANGO_UI_BASE_URL` is configured, and the tenant name [(#12540)](https://github.com/prowler-cloud/prowler/pull/12540)
### 🐞 Fixed
- `POST /api/v1/mute-rules` now updates only each affected provider's latest completed scan and future scans, preventing historical reaggregation from flooding Celery queues [(#12681)](https://github.com/prowler-cloud/prowler/pull/12681)
---
## [1.41.0] (Prowler v5.40.0)
### 🐞 Fixed
- `FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS` is now applied in months instead of days, and negative values are rejected [(#12580)](https://github.com/prowler-cloud/prowler/pull/12580)
### 🔐 Security
- `sqlparse` upgraded to 0.6.0, patching CVE-2026-54284, CVE-2026-59893, and CVE-2026-71491 [(#12509)](https://github.com/prowler-cloud/prowler/pull/12509)
- `openssl`, `libssl3t64` and `openssl-provider-legacy` upgraded to 3.5.7-1~deb13u2 in the API container image, patching ten high OpenSSL CVEs [(#12549)](https://github.com/prowler-cloud/prowler/pull/12549)
---
## [1.40.1] (Prowler v5.39.1)
### 🔄 Changed
- Bump alibabacloud-tea-openapi to 0.4.6, oci to 2.184.1 and pyopenssl to 26.4.0 to match the SDK; the cryptography override now names its actual blockers (azure-cli-core pins msal below 1.37, workos 8.3.0 requires cryptography 48) [(#12477)](https://github.com/prowler-cloud/prowler/pull/12477)
### 🐞 Fixed
- Pin zstd to 1.5.7.2; 1.5.7.3 was yanked from PyPI as not thread safe [(#12477)](https://github.com/prowler-cloud/prowler/pull/12477)
### 🔐 Security
- Trivy from v0.72.0 to v0.73.0 in the container image, fixing HIGH CVE-2026-46600 in the bundled `golang.org/x/net` [(#12445)](https://github.com/prowler-cloud/prowler/pull/12445)
- Trivy v0.74.0 and Debian util-linux 2.41.5-0+deb13u1 in the API container image, patching Go standard library vulnerabilities and CVE-2026-53615 [(#12470)](https://github.com/prowler-cloud/prowler/pull/12470)
---
## [1.40.0] (Prowler v5.39.0)
### 🔄 Changed
+13 -3
View File
@@ -7,20 +7,28 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.72.0
ARG TRIVY_VERSION=0.74.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
ARG TRIVY_SHA256_AMD64=bbb64b9695866ce4a7a8f5c9592002c5961cab378577fa3f8a040df362b9b2ea
ARG TRIVY_SHA256_ARM64=2ca2c023109c2db6b2b77366b6717291452d4531167377d95c79547f0c8e3467
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0
ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
# High CVEs fixed in Debian trixie-security but not yet in the pinned base image:
# openssl/libssl3t64/openssl-provider-legacy 3.5.7-1~deb13u2 CVE-2026-14456,
# -14457, -18798, -54874, -63072, -63073, -63074, -63075, -63076, -75803
# (image ships 3.5.6-1~deb13u2)
# Taken as a targeted --only-upgrade rather than by moving the digest: the newest
# published python:3.12-slim-trixie carries the same vulnerable version. The three
# packages are all built from openssl and are flagged separately, so all are named.
# Drop them once the base image ships 3.5.7-1~deb13u2 or later.
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
@@ -36,6 +44,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libtool \
libxslt1-dev \
python3-dev \
&& apt-get install -y --no-install-recommends --only-upgrade \
util-linux libssl3t64 openssl openssl-provider-legacy \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
+14 -10
View File
@@ -61,7 +61,7 @@ dependencies = [
"cartography (==0.138.1)",
"gevent (==25.9.1)",
"werkzeug (==3.1.7)",
"sqlparse (==0.5.5)",
"sqlparse (==0.6.0)",
"fonttools (==4.62.1)",
"uvicorn-worker (==0.4.0)"
]
@@ -71,7 +71,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.40.0"
version = "1.42.0"
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
# target-version tracks this project's lowest supported Python.
@@ -92,8 +92,7 @@ extend-select = [
[tool.uv]
# Transitive pins matching master to avoid silent drift; bump deliberately.
# workos and pyopenssl run ahead of master: the versions master pins cap cryptography
# below 48, so both were bumped to versions that allow it (PROWLER-2310).
# workos is api-only; pyopenssl matches master (PROWLER-2310).
constraint-dependencies = [
"about-time==4.2.1",
"adal==1.2.7",
@@ -130,7 +129,7 @@ constraint-dependencies = [
"alibabacloud-sls20201230==5.9.0",
"alibabacloud-sts20150401==1.1.6",
"alibabacloud-tea==0.4.3",
"alibabacloud-tea-openapi==0.4.5",
"alibabacloud-tea-openapi==0.4.6",
"alibabacloud-tea-util==0.3.14",
"alibabacloud-tea-xml==0.0.3",
"alibabacloud-vpc20160428==6.13.0",
@@ -339,7 +338,7 @@ constraint-dependencies = [
"nltk==3.9.4",
"numpy==2.2.6",
"oauthlib==3.3.1",
"oci==2.183.0",
"oci==2.184.1",
"openai==1.109.1",
"openstacksdk==4.2.0",
"opentelemetry-api==1.39.1",
@@ -380,7 +379,7 @@ constraint-dependencies = [
"pylint==3.2.5",
"pymsalruntime==0.18.1",
"pynacl==1.6.2",
"pyopenssl==26.2.0",
"pyopenssl==26.4.0",
"pyparsing==3.3.2",
"pyreadline3==3.5.4",
"pysocks==1.7.1",
@@ -424,7 +423,7 @@ constraint-dependencies = [
"six==1.17.0",
"slack-sdk==3.39.0",
"sniffio==1.3.1",
"sqlparse==0.5.5",
"sqlparse==0.6.0",
"statsd==4.0.1",
"std-uritemplate==2.0.8",
"stevedore==5.6.0",
@@ -458,7 +457,7 @@ constraint-dependencies = [
"zipp==3.23.0",
"zope-event==6.1",
"zope-interface==8.2",
"zstd==1.5.7.3"
"zstd==1.5.7.2"
]
# prowler@master needs okta==3.4.2, but cartography 0.138.1 requires okta<1.0.0.
# Attack Paths does not ingest Okta today, so override the Cartography
@@ -485,7 +484,12 @@ constraint-dependencies = [
# that request pyjwt[crypto] and leave cryptography (needed for RS256) only transitive.
override-dependencies = [
"okta==3.4.2",
# alibabacloud-tea-openapi 0.4.5 caps cryptography below 49 and is the latest release.
# prowler requires cryptography==50.0.0. Two api-only dependencies still cap it below
# 49 and cannot move yet: msal, pinned exactly by azure-cli-core (2.83.0 -> 1.35.0b1,
# 2.89.1 -> 1.36.0, both <49; cartography needs azure-cli-core), and workos 8.3.0
# (~=48.0; workos 10.1.1+ needs ~=50.0 and is a separate SDK upgrade). This api is
# deployed from this lock with `uv sync --locked`, so the override applies to what runs.
# Remove when azure-cli-core pins msal>=1.37.0 and workos is on 10.x.
"cryptography==50.0.0",
"azure-mgmt-containerservice==34.1.0",
"microsoft-kiota-abstractions==1.9.10",
+13 -5
View File
@@ -6,6 +6,7 @@ from api.rls import RowLevelSecurityConstraint
from api.uuid_utils import datetime_to_uuid7
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from psqlextra.partitioning import (
PostgresPartitioningError,
PostgresPartitioningManager,
@@ -153,10 +154,17 @@ class PostgresUUIDv7PartitioningStrategy(PostgresRangePartitioningStrategy):
)
def relative_days_or_none(value):
if value is None:
def relative_months_or_none(value):
# A negative value would set the cutoff in the future and delete every
# partition, so it is rejected rather than silently ignored.
if value is not None and value < 0:
raise ImproperlyConfigured(
"FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS must not be negative; "
"leave it unset or use 0 to keep partitions indefinitely"
)
if not value:
return None
return relativedelta(days=value)
return relativedelta(months=value)
#
@@ -173,7 +181,7 @@ manager = PostgresPartitioningManager(
months=settings.FINDINGS_TABLE_PARTITION_MONTHS
),
count=settings.FINDINGS_TABLE_PARTITION_COUNT,
max_age=relative_days_or_none(
max_age=relative_months_or_none(
settings.FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS
),
name_format="%Y_%b",
@@ -189,7 +197,7 @@ manager = PostgresPartitioningManager(
months=settings.FINDINGS_TABLE_PARTITION_MONTHS
),
count=settings.FINDINGS_TABLE_PARTITION_COUNT,
max_age=relative_days_or_none(
max_age=relative_months_or_none(
settings.FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS
),
name_format="%Y_%b",
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.40.0
version: 1.42.0
description: |-
Prowler API specification.
@@ -0,0 +1,63 @@
from datetime import UTC, datetime
from itertools import islice
import pytest
from api.partitions import (
PostgresUUIDv7PartitioningStrategy,
relative_months_or_none,
)
from dateutil.relativedelta import relativedelta
from django.core.exceptions import ImproperlyConfigured
from psqlextra.partitioning import PostgresTimePartitionSize
def build_strategy(max_age):
return PostgresUUIDv7PartitioningStrategy(
size=PostgresTimePartitionSize(months=1),
count=1,
start_date=datetime.now(UTC),
max_age=max_age,
name_format="%Y_%b",
)
class TestRelativeMonthsOrNone:
@pytest.mark.parametrize("value", [None, 0])
def test_unset_or_zero_keeps_partitions_indefinitely(self, value):
assert relative_months_or_none(value) is None
@pytest.mark.parametrize("months", [1, 3, 12])
def test_value_is_interpreted_as_months(self, months):
assert relative_months_or_none(months) == relativedelta(months=months)
def test_value_is_not_interpreted_as_days(self):
assert relative_months_or_none(12) != relativedelta(days=12)
def test_negative_is_rejected(self):
with pytest.raises(ImproperlyConfigured):
relative_months_or_none(-12)
class TestToDelete:
@pytest.mark.parametrize("max_age", [None, relative_months_or_none(0)])
def test_nothing_is_deleted_without_max_age(self, max_age):
strategy = build_strategy(max_age)
assert list(islice(strategy.to_delete(), 5)) == []
def test_first_deleted_partition_is_max_age_old(self):
months = 3
strategy = build_strategy(relative_months_or_none(months))
first = next(strategy.to_delete())
expected = strategy.get_start_datetime() - relativedelta(months=months)
assert first.name() == expected.strftime("%Y_%b").lower()
def test_deleted_partitions_go_further_back_in_time(self):
strategy = build_strategy(relative_months_or_none(3))
names = [p.name() for p in islice(strategy.to_delete(), 3)]
starts = [datetime.strptime(n, "%Y_%b") for n in names]
assert starts == sorted(starts, reverse=True)
+18 -27
View File
@@ -18333,19 +18333,14 @@ class TestMuteRuleViewSet:
assert len(data) == 2
assert data[0]["id"] == str(mute_rules_fixture[first_index].id)
@patch("api.v1.views.chain")
@patch("api.v1.views.reaggregate_all_finding_group_summaries_task.si")
@patch("api.v1.views.mute_historical_findings_task.si")
@patch("api.v1.views.mute_findings_in_latest_scans_task.apply_async")
@patch("api.v1.views.transaction.on_commit", side_effect=lambda fn: fn())
def test_mute_rules_create_valid(
self,
_mock_on_commit,
mock_mute_signature,
mock_reaggregate_signature,
mock_chain,
mock_mute_task,
authenticated_client,
findings_fixture,
create_test_user,
):
"""Test creating a valid mute rule."""
finding_ids = [str(findings_fixture[0].id)]
@@ -18372,24 +18367,20 @@ class TestMuteRuleViewSet:
assert response_data["attributes"]["name"] == "New Mute Rule"
assert response_data["attributes"]["reason"] == "Security exception approved"
# Verify the finding was immediately muted
from api.models import Finding
finding = Finding.objects.get(id=findings_fixture[0].id)
assert finding.muted is True
assert finding.muted_at is not None
assert finding.muted_reason == "Security exception approved"
assert finding.muted is False
assert finding.muted_at is None
assert finding.muted_reason is None
# Verify background task chain was called: mute → reaggregate all
mock_mute_signature.assert_called_once()
mock_reaggregate_signature.assert_called_once()
mock_chain.assert_called_once_with(
mock_mute_signature.return_value,
mock_reaggregate_signature.return_value,
mock_mute_task.assert_called_once_with(
kwargs={
"tenant_id": str(finding.tenant_id),
"mute_rule_id": response_data["id"],
"provider_ids": [str(finding.scan.provider_id)],
}
)
mock_chain.return_value.apply_async.assert_called_once()
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
@patch("api.v1.views.mute_findings_in_latest_scans_task.apply_async")
def test_mute_rules_create_converts_finding_ids_to_uids(
self,
mock_task,
@@ -18425,7 +18416,7 @@ class TestMuteRuleViewSet:
]
assert set(mute_rule.finding_uids) == set(expected_uids)
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
@patch("api.v1.views.mute_findings_in_latest_scans_task.apply_async")
def test_mute_rules_deduplicates_uids(
self,
mock_task,
@@ -18492,10 +18483,10 @@ class TestMuteRuleViewSet:
finding1.refresh_from_db()
finding2.refresh_from_db()
assert finding1.muted is True
assert finding2.muted is True
assert finding1.muted is False
assert finding2.muted is False
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
@patch("api.v1.views.mute_findings_in_latest_scans_task.apply_async")
def test_mute_rules_create_overlap_detection_active(
self,
mock_task,
@@ -18528,7 +18519,7 @@ class TestMuteRuleViewSet:
"already muted" in error_detail.lower() or "overlap" in error_detail.lower()
)
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
@patch("api.v1.views.mute_findings_in_latest_scans_task.apply_async")
def test_mute_rules_create_no_overlap_with_inactive(
self,
mock_task,
@@ -18584,7 +18575,7 @@ class TestMuteRuleViewSet:
== "/data/attributes/finding_ids"
)
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
@patch("api.v1.views.mute_findings_in_latest_scans_task.apply_async")
def test_mute_rules_create_invalid_finding_ids(
self, mock_task, authenticated_client
):
+18 -27
View File
@@ -244,7 +244,6 @@ from api.v1.serializers import (
UserUpdateSerializer,
)
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
from celery import chain
from celery.result import AsyncResult
from config.custom_logging import BackendLogger
from config.env import env
@@ -342,8 +341,7 @@ from tasks.tasks import (
enqueue_scan_execution_on_commit,
get_active_provider_scan,
jira_integration_task,
mute_historical_findings_task,
reaggregate_all_finding_group_summaries_task,
mute_findings_in_latest_scans_task,
refresh_lighthouse_provider_models_task,
)
@@ -7551,35 +7549,28 @@ class MuteRuleViewSet(BaseRLSViewSet):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Create the mute rule
tenant_id = str(request.tenant_id)
finding_ids = serializer.validated_data["finding_ids"]
provider_ids = list(
dict.fromkeys(
Finding.all_objects.filter(
id__in=finding_ids, tenant_id=tenant_id
).values_list("scan__provider_id", flat=True)
)
)
mute_rule = serializer.save()
tenant_id = str(request.tenant_id)
finding_ids = request.data.get("finding_ids", [])
# Immediately mute the selected findings
Finding.all_objects.filter(
id__in=finding_ids, tenant_id=tenant_id, muted=False
).update(
muted=True,
muted_at=mute_rule.inserted_at,
muted_reason=mute_rule.reason,
)
# Launch background task for historical muting + reaggregation
transaction.on_commit(
lambda: chain(
mute_historical_findings_task.si(
tenant_id=tenant_id,
mute_rule_id=str(mute_rule.id),
),
reaggregate_all_finding_group_summaries_task.si(
tenant_id=tenant_id,
),
).apply_async()
lambda: mute_findings_in_latest_scans_task.apply_async(
kwargs={
"tenant_id": tenant_id,
"mute_rule_id": str(mute_rule.id),
"provider_ids": [str(provider_id) for provider_id in provider_ids],
}
)
)
# Return the created mute rule
serializer = self.get_serializer(mute_rule)
return Response(
data=serializer.data,
+5
View File
@@ -303,6 +303,11 @@ SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
DJANGO_DELETION_BATCH_SIZE = env.int("DJANGO_DELETION_BATCH_SIZE", 5000)
# Public base URL of the Prowler UI (for example https://cloud.prowler.com). Used to
# build links back to findings in outbound integrations such as Jira. Empty by
# default, so self-hosted deployments emit no links unless they configure it.
UI_BASE_URL = env.str("DJANGO_UI_BASE_URL", "").rstrip("/")
# SAML requirement
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
+72 -1
View File
@@ -2,13 +2,16 @@ import os
import time
from datetime import UTC, datetime
from glob import glob
from urllib.parse import quote
from api.db_router import READ_REPLICA_ALIAS, MainRouter
from api.db_utils import REPLICA_MAX_ATTEMPTS, REPLICA_RETRY_BASE_DELAY, rls_transaction
from api.models import Finding, Integration, Provider
from api.rls import Tenant
from api.utils import initialize_prowler_integration, initialize_prowler_provider
from celery.utils.log import get_task_logger
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE
from django.conf import settings
from django.db import OperationalError
from prowler.lib.outputs.asff.asff import ASFF
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
@@ -16,6 +19,7 @@ from prowler.lib.outputs.csv.csv import CSV
from prowler.lib.outputs.finding import Finding as FindingOutput
from prowler.lib.outputs.html.html import HTML
from prowler.lib.outputs.jira.exceptions.exceptions import JiraBaseException
from prowler.lib.outputs.jira.jira import Jira
from prowler.lib.outputs.ocsf.ocsf import OCSF
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.s3.s3 import S3
@@ -477,6 +481,55 @@ def upload_security_hub_integration(
return False
JIRA_LABEL_PREFIX = "prowler"
def build_jira_finding_url(finding_uid: str) -> str:
"""Build the Prowler UI link for a finding, or "" when no UI base URL is set.
The link filters by the finding ``uid`` rather than the per-scan record id so
it keeps resolving after the finding is seen again in later scans.
"""
base_url = getattr(settings, "UI_BASE_URL", "")
if not base_url or not finding_uid:
return ""
return f"{base_url}/findings?filter[uid]={quote(finding_uid, safe='')}"
def build_jira_issue_labels(
finding_uid: str, provider: str, severity: str, check_id: str
) -> list[str]:
"""Build the deterministic label set written to every Jira issue.
Labels are prefixed to avoid colliding with customer labels and sanitized so
Jira never rejects them; the finding-uid label is what lets a ticket be traced
back (or JQL-filtered) to its finding.
"""
raw_labels = [
JIRA_LABEL_PREFIX,
f"{JIRA_LABEL_PREFIX}-{provider}" if provider else "",
f"{JIRA_LABEL_PREFIX}-{severity}" if severity else "",
f"{JIRA_LABEL_PREFIX}-{check_id}" if check_id else "",
Jira.build_finding_label(finding_uid),
]
return Jira.sanitize_labels(raw_labels)
def get_tenant_name(tenant_id: str) -> str:
"""Return the tenant name for the Jira issue "Tenant Info" row, or "" if unknown.
The name is informational only, so a lookup failure must never block the send.
"""
try:
return (
Tenant.objects.filter(id=tenant_id).values_list("name", flat=True).first()
or ""
)
except Exception:
logger.warning("Could not resolve tenant name for %s", tenant_id)
return ""
def send_findings_to_jira(
tenant_id: str,
integration_id: str,
@@ -487,6 +540,7 @@ def send_findings_to_jira(
with rls_transaction(tenant_id):
integration = Integration.objects.get(id=integration_id)
jira_integration = initialize_prowler_integration(integration)
tenant_info = get_tenant_name(tenant_id)
num_tickets_created = 0
error_messages = []
@@ -519,6 +573,15 @@ def send_findings_to_jira(
recommendation = remediation.get("recommendation", {})
remediation_code = remediation.get("code", {})
provider_type = finding_instance.scan.provider.provider
issue_labels = build_jira_issue_labels(
finding_uid=finding_instance.uid,
provider=provider_type,
severity=finding_instance.severity,
check_id=finding_instance.check_id,
)
finding_url = build_jira_finding_url(finding_instance.uid)
try:
# Send the individual finding to Jira
result = jira_integration.send_finding(
@@ -527,7 +590,7 @@ def send_findings_to_jira(
severity=finding_instance.severity,
status=finding_instance.status,
status_extended=finding_instance.status_extended or "",
provider=finding_instance.scan.provider.provider,
provider=provider_type,
region=region,
resource_uid=resource_uid,
resource_name=resource_name,
@@ -542,6 +605,9 @@ def send_findings_to_jira(
compliance=finding_instance.compliance or {},
project_key=project_key,
issue_type=issue_type,
issue_labels=issue_labels,
finding_url=finding_url,
tenant_info=tenant_info,
)
except JiraBaseException as error:
error_message = error.message or JIRA_GENERIC_SEND_ERROR
@@ -557,6 +623,11 @@ def send_findings_to_jira(
if result:
num_tickets_created += 1
logger.info(
"Finding %s sent to Jira as %s",
finding_id,
result.get("key") if isinstance(result, dict) else result,
)
else:
error_message = JIRA_GENERIC_SEND_ERROR
logger.error(error_message)
+86 -45
View File
@@ -1,63 +1,104 @@
from collections.abc import Iterable
from api.db_utils import rls_transaction
from api.models import Finding, MuteRule
from api.models import Finding, MuteRule, Scan, StateChoices
from celery.utils.log import get_task_logger
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE
from tasks.utils import batched
logger = get_task_logger(__name__)
def mute_historical_findings(tenant_id: str, mute_rule_id: str):
"""
Mute historical findings that match the given mute rule.
def _mute_findings_for_rule(
*,
tenant_id: str,
scan_id: str,
finding_uids: Iterable[str],
muted_at,
muted_reason: str,
) -> int:
finding_uids = list(finding_uids)
if not finding_uids:
return 0
This function processes findings in batches, updating their muted status
and adding the mute reason.
return Finding.all_objects.filter(
tenant_id=tenant_id,
scan_id=scan_id,
uid__in=finding_uids,
muted=False,
).update(
muted=True,
muted_at=muted_at,
muted_reason=muted_reason,
)
Args:
tenant_id (str): The tenant ID for RLS context
mute_rule_id (str): The ID of the mute rule to apply
Returns:
dict: Summary of the muting operation with findings_muted count
"""
findings_muted_count = 0
def mute_findings_in_latest_scans(
tenant_id: str, mute_rule_id: str, provider_ids: list[str]
) -> dict:
"""Apply a mute rule to the latest completed scan of each provider."""
provider_ids = list(dict.fromkeys(provider_ids))
# Get the list of UIDs to mute and the reason
with rls_transaction(tenant_id):
mute_rule = MuteRule.objects.get(id=mute_rule_id, tenant_id=tenant_id)
finding_uids = mute_rule.finding_uids
mute_reason = mute_rule.reason
muted_at = mute_rule.inserted_at
# Query findings that match the UIDs and are not already muted
with rls_transaction(tenant_id):
findings_to_mute = Finding.objects.filter(
tenant_id=tenant_id, uid__in=finding_uids, muted=False
)
total_findings = findings_to_mute.count()
logger.info(
f"Processing {total_findings} findings for mute rule {mute_rule_id}"
latest_scans = list(
Scan.objects.filter(
tenant_id=tenant_id,
provider_id__in=provider_ids,
state=StateChoices.COMPLETED,
completed_at__isnull=False,
)
.order_by("provider_id", "-completed_at", "-inserted_at", "-id")
.distinct("provider_id")
.values_list("id", flat=True)
)
if total_findings > 0:
for batch, is_last in batched(
findings_to_mute.iterator(), DJANGO_FINDINGS_BATCH_SIZE
):
batch_ids = [f.id for f in batch]
updated_count = Finding.all_objects.filter(
id__in=batch_ids, tenant_id=tenant_id
).update(
muted=True,
muted_at=muted_at,
muted_reason=mute_reason,
)
findings_muted_count += updated_count
logger.info(f"Muted {findings_muted_count} findings for rule {mute_rule_id}")
changed_scan_ids = []
findings_muted = 0
for scan_id in latest_scans:
updated = _mute_findings_for_rule(
tenant_id=tenant_id,
scan_id=str(scan_id),
finding_uids=mute_rule.finding_uids,
muted_at=mute_rule.inserted_at,
muted_reason=mute_rule.reason,
)
if updated:
findings_muted += updated
changed_scan_ids.append(str(scan_id))
logger.info(
"Muted %d findings in %d latest scans for rule %s",
findings_muted,
len(changed_scan_ids),
mute_rule_id,
)
return {
"findings_muted": findings_muted_count,
"findings_muted": findings_muted,
"rule_id": mute_rule_id,
"scan_ids": changed_scan_ids,
}
def reconcile_scan_mute_rules(tenant_id: str, scan_id: str) -> dict:
"""Apply the current enabled mute rules to one completed scan."""
findings_muted = 0
with rls_transaction(tenant_id):
mute_rules = MuteRule.objects.filter(tenant_id=tenant_id, enabled=True).values(
"finding_uids", "reason", "inserted_at"
)
for mute_rule in mute_rules:
findings_muted += _mute_findings_for_rule(
tenant_id=tenant_id,
scan_id=scan_id,
finding_uids=mute_rule["finding_uids"],
muted_at=mute_rule["inserted_at"],
muted_reason=mute_rule["reason"],
)
logger.info(
"Reconciled mute rules for scan %s; muted %d findings",
scan_id,
findings_muted,
)
return {"findings_muted": findings_muted, "scan_id": str(scan_id)}
+45 -99
View File
@@ -73,7 +73,10 @@ from tasks.jobs.lighthouse_providers import (
check_lighthouse_provider_connection,
refresh_lighthouse_provider_models,
)
from tasks.jobs.muting import mute_historical_findings
from tasks.jobs.muting import (
mute_findings_in_latest_scans,
reconcile_scan_mute_rules,
)
from tasks.jobs.orphan_recovery import reconcile_orphans
from tasks.jobs.report import (
STALE_TMP_OUTPUT_MAX_AGE_HOURS,
@@ -526,6 +529,7 @@ def perform_scan_task(
provider_id=provider_id,
checks_to_execute=checks_to_execute,
)
reconcile_scan_mute_rules(tenant_id, scan_id)
_perform_scan_complete_tasks(tenant_id, scan_id, provider_id)
return result
finally:
@@ -635,6 +639,7 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
scan_id=str(scan_instance.id),
provider_id=provider_id,
)
reconcile_scan_mute_rules(tenant_id, str(scan_instance.id))
_perform_scan_complete_tasks(tenant_id, str(scan_instance.id), provider_id)
return result
finally:
@@ -1188,85 +1193,48 @@ def aggregate_finding_group_summaries_task(tenant_id: str, scan_id: str):
return aggregate_finding_group_summaries(tenant_id=tenant_id, scan_id=scan_id)
@shared_task(
base=RLSTask, name="reaggregate-all-finding-group-summaries", queue="overview"
)
@set_tenant(keep_tenant=True)
def reaggregate_all_finding_group_summaries_task(tenant_id: str):
"""Reaggregate every pre-aggregated summary table for this tenant.
def _dispatch_scan_summary_reaggregation(tenant_id: str, scan_ids: list[str]) -> None:
if not scan_ids:
return
Mirrors the unbounded scope of `mute_historical_findings_task`: that task
rewrites every Finding row whose UID matches a mute rule, with no time
limit. To keep the pre-aggregated tables consistent with that update,
this task re-runs the same per-scan aggregation pipeline that scan
completion runs on the latest completed scan of every (provider, day)
pair, rebuilding the tables that power the read endpoints:
- `ScanSummary` and `DailySeveritySummary` -> `/overviews/findings`,
`/overviews/findings-severity`, `/overviews/services`.
- `FindingGroupDailySummary` -> `/finding-groups` and
`/finding-groups/latest`.
- `ScanGroupSummary` -> `/overviews/resource-groups` (resource
inventory).
- `ScanCategorySummary` -> `/overviews/categories`.
- `AttackSurfaceOverview` -> `/overviews/attack-surfaces`.
Per-scan pipelines are dispatched in parallel via a Celery group so
wallclock scales with the worker pool.
"""
completed_scans = list(
Scan.objects.filter(
tenant_id=tenant_id,
state=StateChoices.COMPLETED,
completed_at__isnull=False,
)
.order_by("-completed_at")
.values("id", "completed_at", "provider_id")
logger.info(
"Reaggregating overview/finding summaries for %d latest scans",
len(scan_ids),
)
# Keep the latest scan per (provider, day) pair so the daily summary row
# the aggregator writes is the most recent snapshot of that day for that
# provider. Iterating from most recent to oldest means the first scan we
# see for a given key wins.
latest_scans: dict[tuple, str] = {}
for scan in completed_scans:
key = (scan["provider_id"], scan["completed_at"].date())
if key not in latest_scans:
latest_scans[key] = str(scan["id"])
scan_ids = list(latest_scans.values())
if scan_ids:
logger.info(
"Reaggregating overview/finding summaries for %d scans (provider x day)",
len(scan_ids),
)
# DailySeveritySummary reads from ScanSummary, so ScanSummary must be
# recomputed first; the other aggregators read Finding directly and
# can run in parallel with the severity step.
group(
chain(
perform_scan_summary_task.si(tenant_id=tenant_id, scan_id=scan_id),
group(
aggregate_daily_severity_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
aggregate_finding_group_summaries_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
aggregate_scan_resource_group_summaries_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
aggregate_scan_category_summaries_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
aggregate_attack_surface_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
group(
chain(
perform_scan_summary_task.si(tenant_id=tenant_id, scan_id=scan_id),
group(
aggregate_daily_severity_task.si(tenant_id=tenant_id, scan_id=scan_id),
aggregate_finding_group_summaries_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
)
for scan_id in scan_ids
).apply_async()
return {"scans_reaggregated": len(scan_ids)}
aggregate_scan_resource_group_summaries_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
aggregate_scan_category_summaries_task.si(
tenant_id=tenant_id, scan_id=scan_id
),
aggregate_attack_surface_task.si(tenant_id=tenant_id, scan_id=scan_id),
),
)
for scan_id in scan_ids
).apply_async()
@shared_task(base=RLSTask, name="findings-mute-latest-scans", queue="overview")
@set_tenant(keep_tenant=True)
def mute_findings_in_latest_scans_task(
tenant_id: str, mute_rule_id: str, provider_ids: list[str]
):
"""Apply a mute rule to current scans and rebuild only changed summaries."""
result = mute_findings_in_latest_scans(
tenant_id=tenant_id,
mute_rule_id=mute_rule_id,
provider_ids=provider_ids,
)
_dispatch_scan_summary_reaggregation(tenant_id, result["scan_ids"])
return result
@shared_task(base=RLSTask, name="lighthouse-connection-check")
@@ -1467,25 +1435,3 @@ def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id:
generate_csa=True,
generate_cis=True,
)
@shared_task(name="findings-mute-historical")
def mute_historical_findings_task(tenant_id: str, mute_rule_id: str):
"""
Background task to mute all historical findings matching a mute rule.
This task processes findings in batches to avoid memory issues with large datasets.
It updates the Finding.muted, Finding.muted_at, and Finding.muted_reason fields
for all findings whose UID is in the mute rule's finding_uids list.
Args:
tenant_id (str): The tenant ID for RLS context.
mute_rule_id (str): The primary key of the MuteRule to apply.
Returns:
dict: A dictionary containing:
- 'findings_muted' (int): Total number of findings muted.
- 'rule_id' (str): The mute rule ID.
- 'status' (str): Final status ('completed').
"""
return mute_historical_findings(tenant_id, mute_rule_id)
@@ -6,15 +6,20 @@ from api.db_router import READ_REPLICA_ALIAS, MainRouter
from api.models import Integration
from api.utils import prowler_integration_connection_test
from django.db import OperationalError
from django.test import override_settings
from prowler.lib.outputs.jira.exceptions.exceptions import (
JiraRefreshTokenError,
JiraRequiredCustomFieldsError,
)
from prowler.lib.outputs.jira.jira import Jira
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection
from prowler.providers.common.models import Connection
from tasks.jobs.integrations import (
build_jira_finding_url,
build_jira_issue_labels,
get_s3_client_from_integration,
get_security_hub_client_from_integration,
get_tenant_name,
send_findings_to_jira,
upload_s3_integration,
upload_security_hub_integration,
@@ -1696,6 +1701,7 @@ class TestJiraIntegration:
finding1 = MagicMock()
finding1.id = "finding-1"
finding1.uid = "prowler-aws-check_001-123456789012-us-east-1-my bucket"
finding1.check_id = "check_001"
finding1.severity = "high"
finding1.status = "FAIL"
@@ -1724,6 +1730,7 @@ class TestJiraIntegration:
finding2 = MagicMock()
finding2.id = "finding-2"
finding2.uid = "prowler-azure-check_002-sub/resource"
finding2.check_id = "check_002"
finding2.severity = "medium"
finding2.status = "PASS"
@@ -1748,9 +1755,13 @@ class TestJiraIntegration:
]
# Call the function
result = send_findings_to_jira(
tenant_id, integration_id, project_key, issue_type, finding_ids
)
with (
override_settings(UI_BASE_URL="https://cloud.example.com"),
patch("tasks.jobs.integrations.get_tenant_name", return_value="Acme"),
):
result = send_findings_to_jira(
tenant_id, integration_id, project_key, issue_type, finding_ids
)
# Assertions
assert result == {"created_count": 2, "failed_count": 0}
@@ -1773,12 +1784,36 @@ class TestJiraIntegration:
assert first_call.kwargs["provider"] == "aws"
assert first_call.kwargs["project_key"] == project_key
assert first_call.kwargs["issue_type"] == issue_type
# Finding reference: labels, link back and tenant info
assert first_call.kwargs["issue_labels"] == [
"prowler",
"prowler-aws",
"prowler-high",
"prowler-check_001",
"prowler-finding-prowler-aws-check_001-123456789012-us-east-1-my_bucket",
]
assert first_call.kwargs["finding_url"] == (
"https://cloud.example.com/findings?filter[uid]="
"prowler-aws-check_001-123456789012-us-east-1-my%20bucket"
)
assert first_call.kwargs["tenant_info"] == "Acme"
# Verify second call
second_call = mock_jira_integration.send_finding.call_args_list[1]
assert second_call.kwargs["check_id"] == "check_002"
assert second_call.kwargs["severity"] == "medium"
assert second_call.kwargs["status"] == "PASS"
assert second_call.kwargs["issue_labels"] == [
"prowler",
"prowler-azure",
"prowler-medium",
"prowler-check_002",
"prowler-finding-prowler-azure-check_002-sub/resource",
]
assert second_call.kwargs["finding_url"] == (
"https://cloud.example.com/findings?filter[uid]="
"prowler-azure-check_002-sub%2Fresource"
)
@patch("tasks.jobs.integrations.rls_transaction")
@patch("tasks.jobs.integrations.Finding")
@@ -2200,3 +2235,101 @@ class TestJiraIntegration:
assert call_kwargs["remediation_code_cli"] == ""
assert call_kwargs["remediation_code_other"] == ""
assert call_kwargs["compliance"] == {}
class TestJiraFindingReference:
"""Helpers that give Jira issues a stable reference back to the finding."""
def test_build_jira_issue_labels(self):
assert build_jira_issue_labels(
finding_uid="prowler-aws-check-123-eu-west-1-hub/unknown",
provider="aws",
severity="critical",
check_id="iam_root_mfa",
) == [
"prowler",
"prowler-aws",
"prowler-critical",
"prowler-iam_root_mfa",
"prowler-finding-prowler-aws-check-123-eu-west-1-hub/unknown",
]
def test_build_jira_issue_labels_skips_empty_parts(self):
assert build_jira_issue_labels(
finding_uid="", provider="", severity="", check_id=""
) == ["prowler"]
def test_build_jira_issue_labels_sanitizes_metadata(self):
assert build_jira_issue_labels(
finding_uid=" uid\x00 with spaces ",
provider="aws cloud",
severity="high severity",
check_id="check id",
) == [
"prowler",
"prowler-aws_cloud",
"prowler-high_severity",
"prowler-check_id",
"prowler-finding-uid_with_spaces",
]
def test_build_jira_issue_labels_preserves_maximum_length_uid(self):
finding_uid = "u" * (Jira.LABEL_MAX_LENGTH - len(Jira.FINDING_LABEL_PREFIX) - 1)
finding_label = build_jira_issue_labels(
finding_uid=finding_uid,
provider="gcp",
severity="low",
check_id="check",
)[-1]
assert finding_label == f"{Jira.FINDING_LABEL_PREFIX}-{finding_uid}"
assert len(finding_label) == Jira.LABEL_MAX_LENGTH
def test_build_jira_issue_labels_distinguishes_long_uids(self):
common_prefix = "u" * 300
first_uid = f"{common_prefix}-first"
second_uid = f"{common_prefix}-second"
first_label = build_jira_issue_labels(
finding_uid=first_uid,
provider="gcp",
severity="low",
check_id="check",
)[-1]
second_label = build_jira_issue_labels(
finding_uid=second_uid,
provider="gcp",
severity="low",
check_id="check",
)[-1]
assert first_label == Jira.build_finding_label(first_uid)
assert second_label == Jira.build_finding_label(second_uid)
assert first_label != second_label
assert len(first_label) == Jira.LABEL_MAX_LENGTH
assert len(second_label) == Jira.LABEL_MAX_LENGTH
@override_settings(UI_BASE_URL="")
def test_build_jira_finding_url_without_base_url(self):
assert build_jira_finding_url("prowler-aws-check-1") == ""
@override_settings(UI_BASE_URL="https://cloud.example.com")
def test_build_jira_finding_url_with_base_url(self):
assert build_jira_finding_url("prowler-aws-check-1") == (
"https://cloud.example.com/findings?filter[uid]=prowler-aws-check-1"
)
# uid characters that would break the query string are encoded
assert build_jira_finding_url("a/b c&d") == (
"https://cloud.example.com/findings?filter[uid]=a%2Fb%20c%26d"
)
assert build_jira_finding_url("") == ""
@pytest.mark.django_db
def test_get_tenant_name(self, tenants_fixture):
tenant = tenants_fixture[0]
assert get_tenant_name(str(tenant.id)) == tenant.name
@pytest.mark.django_db
def test_get_tenant_name_unknown_or_invalid(self):
assert get_tenant_name("00000000-0000-0000-0000-000000000000") == ""
assert get_tenant_name("not-a-uuid") == ""
+176 -502
View File
@@ -1,531 +1,205 @@
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from uuid import uuid4
import pytest
from api.models import Finding, MuteRule
from django.core.exceptions import ObjectDoesNotExist
from api.models import Finding, MuteRule, Scan, StateChoices
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
from tasks.jobs.muting import mute_historical_findings
from tasks.jobs.muting import (
mute_findings_in_latest_scans,
reconcile_scan_mute_rules,
)
def _create_finding(scan: Scan, uid: str) -> Finding:
return Finding.objects.create(
tenant_id=scan.tenant_id,
uid=uid,
scan=scan,
status=Status.FAIL,
status_extended="Test finding",
impact=Severity.high,
severity=Severity.high,
raw_result={},
check_id="test_check",
check_metadata={"CheckId": "test_check"},
muted=False,
)
def _create_mute_rule(tenant_id, user, finding_uids, *, enabled=True) -> MuteRule:
return MuteRule.objects.create(
tenant_id=tenant_id,
name=f"Mute rule {uuid4()}",
reason="Approved exception",
enabled=enabled,
created_by=user,
finding_uids=finding_uids,
)
@pytest.mark.django_db
class TestMuteHistoricalFindings:
"""
Test suite for the mute_historical_findings function.
class TestMuteFindingsInLatestScans:
def test_mutes_latest_scan_and_leaves_older_scan_unchanged(
self, scans_fixture, create_test_user
):
latest_scan = scans_fixture[0]
older_scan = Scan.objects.create(
tenant_id=latest_scan.tenant_id,
provider=latest_scan.provider,
name="Older scan",
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
started_at=datetime.now(UTC) - timedelta(days=1),
completed_at=datetime.now(UTC) - timedelta(days=1),
)
uid = "latest-scan-only"
older_finding = _create_finding(older_scan, uid)
latest_finding = _create_finding(latest_scan, uid)
mute_rule = _create_mute_rule(latest_scan.tenant_id, create_test_user, [uid])
This class tests the batch processing of findings to update their muted status
based on MuteRule criteria.
"""
result = mute_findings_in_latest_scans(
str(latest_scan.tenant_id),
str(mute_rule.id),
[str(latest_scan.provider_id)],
)
@pytest.fixture(scope="function")
def test_user(self, create_test_user):
"""Create a test user for mute rule creation."""
return create_test_user
older_finding.refresh_from_db()
latest_finding.refresh_from_db()
assert older_finding.muted is False
assert latest_finding.muted is True
assert latest_finding.muted_at == mute_rule.inserted_at
assert latest_finding.muted_reason == mute_rule.reason
assert result == {
"findings_muted": 1,
"rule_id": str(mute_rule.id),
"scan_ids": [str(latest_scan.id)],
}
@pytest.fixture(scope="function")
def mute_rule_with_findings(self, tenants_fixture, findings_fixture, test_user):
"""
Create a mute rule that targets the first finding in the fixture.
"""
def test_mutes_one_latest_scan_per_provider(self, scans_fixture, create_test_user):
first_scan, second_scan, _ = scans_fixture
uid = "shared-selected-uid"
first_finding = _create_finding(first_scan, uid)
second_finding = _create_finding(second_scan, uid)
mute_rule = _create_mute_rule(first_scan.tenant_id, create_test_user, [uid])
result = mute_findings_in_latest_scans(
str(first_scan.tenant_id),
str(mute_rule.id),
[str(first_scan.provider_id), str(second_scan.provider_id)],
)
first_finding.refresh_from_db()
second_finding.refresh_from_db()
assert first_finding.muted is True
assert second_finding.muted is True
assert result["findings_muted"] == 2
assert set(result["scan_ids"]) == {str(first_scan.id), str(second_scan.id)}
def test_provider_without_completed_scan_does_nothing(
self, tenants_fixture, provider_factory, create_test_user
):
tenant = tenants_fixture[0]
finding = findings_fixture[0]
mute_rule = MuteRule.objects.create(
tenant_id=tenant.id,
name="Test Mute Rule",
reason="Testing mute functionality",
enabled=True,
created_by=test_user,
finding_uids=[finding.uid],
provider = provider_factory()
mute_rule = _create_mute_rule(
tenant.id, create_test_user, ["future-scan-finding"]
)
return mute_rule
result = mute_findings_in_latest_scans(
str(tenant.id), str(mute_rule.id), [str(provider.id)]
)
@pytest.fixture(scope="function")
def mute_rule_multiple_findings(self, scans_fixture, test_user):
"""
Create multiple unmuted findings and a mute rule targeting all of them.
"""
assert result == {
"findings_muted": 0,
"rule_id": str(mute_rule.id),
"scan_ids": [],
}
def test_retry_does_not_report_changed_scans_twice(
self, scans_fixture, create_test_user
):
scan = scans_fixture[0]
tenant_id = scan.tenant_id
# Create 5 unmuted findings
finding_uids = []
for i in range(5):
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"test_finding_uid_mute_{i}",
scan=scan,
status=Status.FAIL,
status_extended=f"Test status {i}",
impact=Severity.high,
severity=Severity.high,
raw_result={
"status": Status.FAIL,
"impact": Severity.high,
"severity": Severity.high,
},
check_id=f"test_check_id_{i}",
check_metadata={
"CheckId": f"test_check_id_{i}",
"Description": f"Test description {i}",
},
muted=False,
)
finding_uids.append(finding.uid)
# Create mute rule targeting all findings
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test Multiple Findings Mute Rule",
reason="Testing batch muting",
enabled=True,
created_by=test_user,
finding_uids=finding_uids,
finding = _create_finding(scan, "idempotent-mute")
mute_rule = _create_mute_rule(scan.tenant_id, create_test_user, [finding.uid])
args = (
str(scan.tenant_id),
str(mute_rule.id),
[str(scan.provider_id)],
)
return mute_rule, finding_uids
first_result = mute_findings_in_latest_scans(*args)
second_result = mute_findings_in_latest_scans(*args)
@pytest.fixture(scope="function")
def mute_rule_already_muted(self, findings_fixture, test_user):
"""
Create a mute rule that targets an already-muted finding.
"""
tenant_id = findings_fixture[1].tenant_id
already_muted_finding = findings_fixture[1]
assert first_result["scan_ids"] == [str(scan.id)]
assert second_result["findings_muted"] == 0
assert second_result["scan_ids"] == []
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test Already Muted Rule",
reason="Testing already muted findings",
enabled=True,
created_by=test_user,
finding_uids=[already_muted_finding.uid],
def test_does_not_cross_tenant_boundary(
self, tenants_fixture, provider_factory, create_test_user
):
tenant = tenants_fixture[0]
other_tenant = tenants_fixture[2]
other_provider = provider_factory(tenant=other_tenant)
other_scan = Scan.objects.create(
tenant_id=other_tenant.id,
provider=other_provider,
name="Other tenant scan",
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
started_at=datetime.now(UTC),
completed_at=datetime.now(UTC),
)
other_finding = _create_finding(other_scan, "tenant-isolated-uid")
mute_rule = _create_mute_rule(tenant.id, create_test_user, [other_finding.uid])
result = mute_findings_in_latest_scans(
str(tenant.id), str(mute_rule.id), [str(other_provider.id)]
)
return mute_rule
other_finding.refresh_from_db()
assert other_finding.muted is False
assert result["scan_ids"] == []
@pytest.fixture(scope="function")
def mute_rule_mixed_findings(self, scans_fixture, test_user):
"""
Create a mute rule with a mix of muted and unmuted findings.
"""
def test_nonexistent_rule_raises(self, tenants_fixture):
with pytest.raises(MuteRule.DoesNotExist):
mute_findings_in_latest_scans(str(tenants_fixture[0].id), str(uuid4()), [])
@pytest.mark.django_db
class TestReconcileScanMuteRules:
def test_applies_only_enabled_rules_to_requested_scan(
self, scans_fixture, create_test_user
):
scan = scans_fixture[0]
tenant_id = scan.tenant_id
# Create 3 unmuted findings
unmuted_uids = []
for i in range(3):
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"unmuted_finding_{i}",
scan=scan,
status=Status.FAIL,
status_extended=f"Unmuted status {i}",
impact=Severity.medium,
severity=Severity.medium,
raw_result={
"status": Status.FAIL,
"impact": Severity.medium,
"severity": Severity.medium,
},
check_id=f"unmuted_check_{i}",
check_metadata={
"CheckId": f"unmuted_check_{i}",
"Description": f"Unmuted description {i}",
},
muted=False,
)
unmuted_uids.append(finding.uid)
# Create 2 already muted findings
muted_uids = []
for i in range(2):
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"muted_finding_{i}",
scan=scan,
status=Status.FAIL,
status_extended=f"Muted status {i}",
impact=Severity.low,
severity=Severity.low,
raw_result={
"status": Status.FAIL,
"impact": Severity.low,
"severity": Severity.low,
},
check_id=f"muted_check_{i}",
check_metadata={
"CheckId": f"muted_check_{i}",
"Description": f"Muted description {i}",
},
muted=True,
muted_at=datetime.now(UTC),
muted_reason="Already muted",
)
muted_uids.append(finding.uid)
# Create mute rule targeting all findings
all_uids = unmuted_uids + muted_uids
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test Mixed Findings Rule",
reason="Testing mixed muted/unmuted findings",
enabled=True,
created_by=test_user,
finding_uids=all_uids,
active_finding = _create_finding(scan, "active-rule-uid")
disabled_finding = _create_finding(scan, "disabled-rule-uid")
active_rule = _create_mute_rule(
scan.tenant_id, create_test_user, [active_finding.uid]
)
return mute_rule, unmuted_uids, muted_uids
@pytest.fixture(scope="function")
def mute_rule_batch_test(self, scans_fixture, test_user):
"""
Create enough findings to test batch processing (>1000 for default batch size).
"""
scan = scans_fixture[0]
tenant_id = scan.tenant_id
# Create 1500 findings to exceed default batch size of 1000
finding_uids = []
for i in range(1500):
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"batch_test_finding_{i}",
scan=scan,
status=Status.FAIL,
status_extended=f"Batch test status {i}",
impact=Severity.critical,
severity=Severity.critical,
raw_result={
"status": Status.FAIL,
"impact": Severity.critical,
"severity": Severity.critical,
},
check_id=f"batch_test_check_{i}",
check_metadata={
"CheckId": f"batch_test_check_{i}",
"Description": f"Batch test description {i}",
},
muted=False,
)
finding_uids.append(finding.uid)
# Create mute rule targeting all findings
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test Batch Processing Rule",
reason="Testing batch processing functionality",
enabled=True,
created_by=test_user,
finding_uids=finding_uids,
_create_mute_rule(
scan.tenant_id,
create_test_user,
[disabled_finding.uid],
enabled=False,
)
return mute_rule, finding_uids
def test_mute_historical_findings_single_finding(
self, mute_rule_with_findings, findings_fixture
):
"""
Test muting a single historical finding.
"""
mute_rule = mute_rule_with_findings
tenant_id = str(mute_rule.tenant_id)
finding = findings_fixture[0]
# Ensure the finding is not muted before execution
finding.refresh_from_db()
assert finding.muted is False
assert finding.muted_at is None
assert finding.muted_reason is None
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify return value
assert result["findings_muted"] == 1
assert result["rule_id"] == str(mute_rule.id)
# Verify the finding was muted
finding.refresh_from_db()
assert finding.muted is True
assert finding.muted_at == mute_rule.inserted_at
assert finding.muted_reason == mute_rule.reason
def test_mute_historical_findings_multiple_findings(
self, mute_rule_multiple_findings
):
"""
Test muting multiple historical findings.
"""
mute_rule, finding_uids = mute_rule_multiple_findings
tenant_id = str(mute_rule.tenant_id)
# Verify all findings are unmuted
findings = Finding.objects.filter(tenant_id=tenant_id, uid__in=finding_uids)
assert findings.count() == 5
for finding in findings:
assert finding.muted is False
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify return value
assert result["findings_muted"] == 5
assert result["rule_id"] == str(mute_rule.id)
# Verify all findings were muted
findings = Finding.objects.filter(tenant_id=tenant_id, uid__in=finding_uids)
for finding in findings:
assert finding.muted is True
assert finding.muted_at == mute_rule.inserted_at
assert finding.muted_reason == mute_rule.reason
def test_mute_historical_findings_already_muted(
self, mute_rule_already_muted, findings_fixture
):
"""
Test that already-muted findings are not counted or updated.
"""
mute_rule = mute_rule_already_muted
tenant_id = str(mute_rule.tenant_id)
finding = findings_fixture[1]
# Verify the finding is already muted
finding.refresh_from_db()
assert finding.muted is True
original_muted_at = finding.muted_at
original_muted_reason = finding.muted_reason
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify no findings were muted
assert result["findings_muted"] == 0
assert result["rule_id"] == str(mute_rule.id)
# Verify the finding's mute status did not change
finding.refresh_from_db()
assert finding.muted is True
assert finding.muted_at == original_muted_at
assert finding.muted_reason == original_muted_reason
def test_mute_historical_findings_mixed_status(self, mute_rule_mixed_findings):
"""
Test muting when some findings are already muted and others are not.
"""
mute_rule, unmuted_uids, muted_uids = mute_rule_mixed_findings
tenant_id = str(mute_rule.tenant_id)
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify only unmuted findings were counted
assert result["findings_muted"] == 3
assert result["rule_id"] == str(mute_rule.id)
# Verify unmuted findings are now muted
unmuted_findings = Finding.objects.filter(
tenant_id=tenant_id, uid__in=unmuted_uids
older_scan = Scan.objects.create(
tenant_id=scan.tenant_id,
provider=scan.provider,
name="Older matching scan",
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
started_at=datetime.now(UTC) - timedelta(days=1),
completed_at=datetime.now(UTC) - timedelta(days=1),
)
for finding in unmuted_findings:
assert finding.muted is True
assert finding.muted_at == mute_rule.inserted_at
assert finding.muted_reason == mute_rule.reason
older_finding = _create_finding(older_scan, active_finding.uid)
# Verify already-muted findings remained unchanged
already_muted_findings = Finding.objects.filter(
tenant_id=tenant_id, uid__in=muted_uids
)
for finding in already_muted_findings:
assert finding.muted is True
assert finding.muted_reason == "Already muted"
result = reconcile_scan_mute_rules(str(scan.tenant_id), str(scan.id))
def test_mute_historical_findings_nonexistent_rule(self, tenants_fixture):
"""
Test that a nonexistent mute rule raises ObjectDoesNotExist.
"""
tenant_id = str(tenants_fixture[0].id)
nonexistent_rule_id = str(uuid4())
with pytest.raises(ObjectDoesNotExist):
mute_historical_findings(tenant_id, nonexistent_rule_id)
def test_mute_historical_findings_no_matching_findings(
self, tenants_fixture, test_user
):
"""
Test muting when no findings match the rule's UIDs.
"""
tenant_id = str(tenants_fixture[0].id)
# Create a mute rule with non-existent finding UIDs
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test No Match Rule",
reason="Testing no matching findings",
enabled=True,
created_by=test_user,
finding_uids=[
"nonexistent_uid_1",
"nonexistent_uid_2",
"nonexistent_uid_3",
],
)
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify no findings were muted
assert result["findings_muted"] == 0
assert result["rule_id"] == str(mute_rule.id)
def test_mute_historical_findings_batch_processing(self, mute_rule_batch_test):
"""
Test that large numbers of findings are processed in batches correctly.
"""
mute_rule, finding_uids = mute_rule_batch_test
tenant_id = str(mute_rule.tenant_id)
# Verify all findings exist and are unmuted
findings = Finding.objects.filter(tenant_id=tenant_id, uid__in=finding_uids)
assert findings.count() == 1500
for finding in findings:
assert finding.muted is False
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify return value
assert result["findings_muted"] == 1500
assert result["rule_id"] == str(mute_rule.id)
# Verify all findings were muted
findings = Finding.objects.filter(tenant_id=tenant_id, uid__in=finding_uids)
for finding in findings:
assert finding.muted is True
assert finding.muted_at == mute_rule.inserted_at
assert finding.muted_reason == mute_rule.reason
def test_mute_historical_findings_preserves_muted_at_timestamp(
self, mute_rule_with_findings, findings_fixture
):
"""
Test that muted_at is set to the rule's inserted_at, not the current time.
"""
mute_rule = mute_rule_with_findings
tenant_id = str(mute_rule.tenant_id)
finding = findings_fixture[0]
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify the finding was muted
assert result["findings_muted"] == 1
# Verify muted_at matches the rule's inserted_at timestamp
finding.refresh_from_db()
assert finding.muted_at == mute_rule.inserted_at
assert finding.muted_at is not None
def test_mute_historical_findings_partial_match(self, scans_fixture, test_user):
"""
Test muting when only some of the rule's UIDs exist as findings.
"""
scan = scans_fixture[0]
tenant_id = str(scan.tenant_id)
# Create 3 findings
existing_uids = []
for i in range(3):
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"partial_match_finding_{i}",
scan=scan,
status=Status.FAIL,
status_extended=f"Partial match status {i}",
impact=Severity.high,
severity=Severity.high,
raw_result={
"status": Status.FAIL,
"impact": Severity.high,
"severity": Severity.high,
},
check_id=f"partial_match_check_{i}",
check_metadata={
"CheckId": f"partial_match_check_{i}",
"Description": f"Partial match description {i}",
},
muted=False,
)
existing_uids.append(finding.uid)
# Create a mute rule with both existing and non-existing UIDs
all_uids = existing_uids + [
"nonexistent_uid_1",
"nonexistent_uid_2",
]
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test Partial Match Rule",
reason="Testing partial matching",
enabled=True,
created_by=test_user,
finding_uids=all_uids,
)
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify only existing findings were muted
assert result["findings_muted"] == 3
assert result["rule_id"] == str(mute_rule.id)
# Verify the existing findings were muted
findings = Finding.objects.filter(tenant_id=tenant_id, uid__in=existing_uids)
assert findings.count() == 3
for finding in findings:
assert finding.muted is True
assert finding.muted_at == mute_rule.inserted_at
assert finding.muted_reason == mute_rule.reason
def test_mute_historical_findings_empty_uids(self, tenants_fixture, test_user):
"""
Test muting when the rule has an empty finding_uids array.
"""
tenant_id = str(tenants_fixture[0].id)
# Create a mute rule with empty finding_uids
mute_rule = MuteRule.objects.create(
tenant_id=tenant_id,
name="Test Empty UIDs Rule",
reason="Testing empty UIDs",
enabled=True,
created_by=test_user,
finding_uids=[],
)
# Execute the muting function
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify no findings were muted
assert result["findings_muted"] == 0
assert result["rule_id"] == str(mute_rule.id)
def test_mute_historical_findings_return_format(self, mute_rule_with_findings):
"""
Test that the return value has the correct format and fields.
"""
mute_rule = mute_rule_with_findings
tenant_id = str(mute_rule.tenant_id)
result = mute_historical_findings(tenant_id, str(mute_rule.id))
# Verify return value structure
assert isinstance(result, dict)
assert "findings_muted" in result
assert "rule_id" in result
assert isinstance(result["findings_muted"], int)
assert isinstance(result["rule_id"], str)
assert result["rule_id"] == str(mute_rule.id)
active_finding.refresh_from_db()
disabled_finding.refresh_from_db()
older_finding.refresh_from_db()
assert active_finding.muted is True
assert active_finding.muted_at == active_rule.inserted_at
assert disabled_finding.muted is False
assert older_finding.muted is False
assert result == {"findings_muted": 1, "scan_id": str(scan.id)}
+69 -124
View File
@@ -1,6 +1,6 @@
import uuid
from contextlib import contextmanager
from datetime import UTC, datetime, timedelta
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import httpx
@@ -33,10 +33,10 @@ from tasks.tasks import (
check_integrations_task,
check_lighthouse_provider_connection_task,
generate_outputs_task,
mute_findings_in_latest_scans_task,
perform_attack_paths_scan_task,
perform_scan_task,
perform_scheduled_scan_task,
reaggregate_all_finding_group_summaries_task,
refresh_lighthouse_provider_models_task,
s3_integration_task,
security_hub_integration_task,
@@ -2959,6 +2959,7 @@ class TestPerformScheduledScanTask:
with (
patch("tasks.tasks.perform_prowler_scan", side_effect=_complete_scan),
patch("tasks.tasks._perform_scan_complete_tasks"),
patch("tasks.tasks.reconcile_scan_mute_rules") as mock_reconcile,
self._override_task_request(perform_scheduled_scan_task, id=task_id),
):
perform_scheduled_scan_task.run(
@@ -2982,6 +2983,13 @@ class TestPerformScheduledScanTask:
).count()
== 1
)
completed_scan = Scan.objects.get(
tenant_id=tenant.id,
provider=provider,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.COMPLETED,
)
mock_reconcile.assert_called_once_with(str(tenant.id), str(completed_scan.id))
assert (
Scan.objects.filter(
tenant_id=tenant.id,
@@ -3176,7 +3184,10 @@ class TestPerformScanTask:
task=queued_task,
)
events = []
def _complete_scan(tenant_id, scan_id, provider_id, checks_to_execute=None):
events.append("scan")
scan_instance = Scan.objects.get(id=scan_id)
scan_instance.state = StateChoices.COMPLETED
scan_instance.save()
@@ -3184,7 +3195,14 @@ class TestPerformScanTask:
with (
patch("tasks.tasks.perform_prowler_scan", side_effect=_complete_scan),
patch("tasks.tasks._perform_scan_complete_tasks"),
patch(
"tasks.tasks.reconcile_scan_mute_rules",
side_effect=lambda *_args: events.append("reconcile"),
),
patch(
"tasks.tasks._perform_scan_complete_tasks",
side_effect=lambda *_args: events.append("summaries"),
),
patch("tasks.tasks.perform_scan_task.apply_async") as mock_apply_async,
):
with django_capture_on_commit_callbacks(execute=True):
@@ -3196,6 +3214,7 @@ class TestPerformScanTask:
queued_task_result.refresh_from_db()
assert result == {"status": "ok"}
assert events == ["scan", "reconcile", "summaries"]
assert queued_task_result.status == states.PENDING
mock_apply_async.assert_called_once_with(
kwargs={
@@ -3241,10 +3260,7 @@ class TestPerformScanTask:
@pytest.mark.django_db
class TestReaggregateAllFindingGroupSummaries:
def setup_method(self):
self.tenant_id = str(uuid.uuid4())
class TestMuteFindingsInLatestScansTask:
@patch("tasks.tasks.chain")
@patch("tasks.tasks.group")
@patch("tasks.tasks.aggregate_attack_surface_task")
@@ -3253,10 +3269,10 @@ class TestReaggregateAllFindingGroupSummaries:
@patch("tasks.tasks.aggregate_finding_group_summaries_task")
@patch("tasks.tasks.aggregate_daily_severity_task")
@patch("tasks.tasks.perform_scan_summary_task")
@patch("tasks.tasks.Scan.objects.filter")
def test_dispatches_subtasks_for_each_provider_per_day(
@patch("tasks.tasks.mute_findings_in_latest_scans")
def test_reaggregates_only_changed_scans(
self,
mock_scan_filter,
mock_mute_findings,
mock_scan_summary_task,
mock_daily_severity_task,
mock_finding_group_task,
@@ -3265,119 +3281,36 @@ class TestReaggregateAllFindingGroupSummaries:
mock_attack_surface_task,
mock_group,
mock_chain,
tenants_fixture,
):
provider_id_1 = uuid.uuid4()
provider_id_2 = uuid.uuid4()
scan_id_today_p1 = uuid.uuid4()
scan_id_yesterday_p1 = uuid.uuid4()
scan_id_today_p2 = uuid.uuid4()
today = datetime.now(tz=UTC)
yesterday = today - timedelta(days=1)
mock_outer_group_result = MagicMock()
# The first `group()` call wraps the inner parallel step; subsequent
# calls wrap the outer per-scan generator.
mock_group.side_effect = lambda *args, **kwargs: (
list(args[0]) if args and hasattr(args[0], "__iter__") else None,
mock_outer_group_result,
)[1]
mock_scan_filter.return_value.order_by.return_value.values.return_value = [
{
"id": scan_id_today_p1,
"completed_at": today,
"provider_id": provider_id_1,
},
{
"id": scan_id_today_p2,
"completed_at": today,
"provider_id": provider_id_2,
},
{
"id": scan_id_yesterday_p1,
"completed_at": yesterday,
"provider_id": provider_id_1,
},
]
result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id)
assert result == {"scans_reaggregated": 3}
expected_scan_ids = {
str(scan_id_today_p1),
str(scan_id_today_p2),
str(scan_id_yesterday_p1),
tenant_id = str(tenants_fixture[0].id)
mute_rule_id = str(uuid.uuid4())
provider_ids = [str(uuid.uuid4()), str(uuid.uuid4())]
scan_ids = [str(uuid.uuid4()), str(uuid.uuid4())]
result = {
"findings_muted": 2,
"rule_id": mute_rule_id,
"scan_ids": scan_ids,
}
for task_mock in (
mock_scan_summary_task,
mock_daily_severity_task,
mock_finding_group_task,
mock_resource_group_task,
mock_category_task,
mock_attack_surface_task,
):
assert task_mock.si.call_count == 3
dispatched = {
call.kwargs["scan_id"] for call in task_mock.si.call_args_list
}
assert dispatched == expected_scan_ids
for call in task_mock.si.call_args_list:
assert call.kwargs["tenant_id"] == self.tenant_id
assert mock_chain.call_count == 3
mock_outer_group_result.apply_async.assert_called_once()
@patch("tasks.tasks.chain")
@patch("tasks.tasks.group")
@patch("tasks.tasks.aggregate_attack_surface_task")
@patch("tasks.tasks.aggregate_scan_category_summaries_task")
@patch("tasks.tasks.aggregate_scan_resource_group_summaries_task")
@patch("tasks.tasks.aggregate_finding_group_summaries_task")
@patch("tasks.tasks.aggregate_daily_severity_task")
@patch("tasks.tasks.perform_scan_summary_task")
@patch("tasks.tasks.Scan.objects.filter")
def test_dedupes_scans_to_latest_per_provider_per_day(
self,
mock_scan_filter,
mock_scan_summary_task,
mock_daily_severity_task,
mock_finding_group_task,
mock_resource_group_task,
mock_category_task,
mock_attack_surface_task,
mock_group,
mock_chain,
):
"""When several scans run on the same day for the same provider, only
the latest one is dispatched (matching the daily summary unique key)."""
provider_id = uuid.uuid4()
latest_scan_today = uuid.uuid4()
earlier_scan_today = uuid.uuid4()
today_late = datetime.now(tz=UTC)
today_early = today_late - timedelta(hours=4)
mock_mute_findings.return_value = result
mock_outer_group_result = MagicMock()
mock_group.side_effect = lambda *args, **kwargs: (
list(args[0]) if args and hasattr(args[0], "__iter__") else None,
mock_outer_group_result,
)[1]
# Returned ordered by `-completed_at`, so the most recent comes first.
mock_scan_filter.return_value.order_by.return_value.values.return_value = [
{
"id": latest_scan_today,
"completed_at": today_late,
"provider_id": provider_id,
},
{
"id": earlier_scan_today,
"completed_at": today_early,
"provider_id": provider_id,
},
]
task_result = mute_findings_in_latest_scans_task(
tenant_id=tenant_id,
mute_rule_id=mute_rule_id,
provider_ids=provider_ids,
)
result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id)
assert result == {"scans_reaggregated": 1}
assert task_result == result
mock_mute_findings.assert_called_once_with(
tenant_id=tenant_id,
mute_rule_id=mute_rule_id,
provider_ids=provider_ids,
)
for task_mock in (
mock_scan_summary_task,
mock_daily_severity_task,
@@ -3386,23 +3319,35 @@ class TestReaggregateAllFindingGroupSummaries:
mock_category_task,
mock_attack_surface_task,
):
task_mock.si.assert_called_once_with(
tenant_id=self.tenant_id, scan_id=str(latest_scan_today)
)
mock_chain.assert_called_once()
assert task_mock.si.call_count == 2
assert {
call.kwargs["scan_id"] for call in task_mock.si.call_args_list
} == set(scan_ids)
assert mock_chain.call_count == 2
mock_outer_group_result.apply_async.assert_called_once()
@patch("tasks.tasks.chain")
@patch("tasks.tasks.group")
@patch("tasks.tasks.Scan.objects.filter")
def test_no_completed_scans_skips_dispatch(
self, mock_scan_filter, mock_group, mock_chain
@patch("tasks.tasks.mute_findings_in_latest_scans")
def test_skips_reaggregation_when_no_scan_changed(
self, mock_mute_findings, mock_group, mock_chain, tenants_fixture
):
mock_scan_filter.return_value.order_by.return_value.values.return_value = []
tenant_id = str(tenants_fixture[0].id)
mute_rule_id = str(uuid.uuid4())
result = {
"findings_muted": 0,
"rule_id": mute_rule_id,
"scan_ids": [],
}
mock_mute_findings.return_value = result
result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id)
task_result = mute_findings_in_latest_scans_task(
tenant_id=tenant_id,
mute_rule_id=mute_rule_id,
provider_ids=[],
)
assert result == {"scans_reaggregated": 0}
assert task_result == result
mock_group.assert_not_called()
mock_chain.assert_not_called()
Generated
+112 -55
View File
@@ -45,7 +45,7 @@ constraints = [
{ name = "alibabacloud-sls20201230", specifier = "==5.9.0" },
{ name = "alibabacloud-sts20150401", specifier = "==1.1.6" },
{ name = "alibabacloud-tea", specifier = "==0.4.3" },
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.5" },
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.6" },
{ name = "alibabacloud-tea-util", specifier = "==0.3.14" },
{ name = "alibabacloud-tea-xml", specifier = "==0.0.3" },
{ name = "alibabacloud-vpc20160428", specifier = "==6.13.0" },
@@ -254,7 +254,7 @@ constraints = [
{ name = "nltk", specifier = "==3.9.4" },
{ name = "numpy", specifier = "==2.2.6" },
{ name = "oauthlib", specifier = "==3.3.1" },
{ name = "oci", specifier = "==2.183.0" },
{ name = "oci", specifier = "==2.184.1" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "openstacksdk", specifier = "==4.2.0" },
{ name = "opentelemetry-api", specifier = "==1.39.1" },
@@ -295,7 +295,7 @@ constraints = [
{ name = "pylint", specifier = "==3.2.5" },
{ name = "pymsalruntime", specifier = "==0.18.1" },
{ name = "pynacl", specifier = "==1.6.2" },
{ name = "pyopenssl", specifier = "==26.2.0" },
{ name = "pyopenssl", specifier = "==26.4.0" },
{ name = "pyparsing", specifier = "==3.3.2" },
{ name = "pyreadline3", specifier = "==3.5.4" },
{ name = "pysocks", specifier = "==1.7.1" },
@@ -339,7 +339,7 @@ constraints = [
{ name = "six", specifier = "==1.17.0" },
{ name = "slack-sdk", specifier = "==3.39.0" },
{ name = "sniffio", specifier = "==1.3.1" },
{ name = "sqlparse", specifier = "==0.5.5" },
{ name = "sqlparse", specifier = "==0.6.0" },
{ name = "statsd", specifier = "==4.0.1" },
{ name = "std-uritemplate", specifier = "==2.0.8" },
{ name = "stevedore", specifier = "==5.6.0" },
@@ -373,7 +373,7 @@ constraints = [
{ name = "zipp", specifier = "==3.23.0" },
{ name = "zope-event", specifier = "==6.1" },
{ name = "zope-interface", specifier = "==8.2" },
{ name = "zstd", specifier = "==1.5.7.3" },
{ name = "zstd", specifier = "==1.5.7.2" },
]
overrides = [
{ name = "azure-mgmt-containerservice", specifier = "==34.1.0" },
@@ -860,7 +860,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0
[[package]]
name = "alibabacloud-tea-openapi"
version = "0.4.5"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alibabacloud-credentials" },
@@ -869,9 +869,9 @@ dependencies = [
{ name = "cryptography" },
{ name = "darabonba-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/73/fb0c4d44759791ecdf269fc715c1e810fa1aba3981bfaaf8a01f61899296/alibabacloud_tea_openapi-0.4.5.tar.gz", hash = "sha256:75fa1f4360a46e41f5bf5f8d4917e52efb6f64885839bc1328c35590670c97b9", size = 26616, upload-time = "2026-07-14T13:15:39.364Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/34/1918a2d780676494365c7f945bfab397ecddb988054d78025bd26f438977/alibabacloud_tea_openapi-0.4.6.tar.gz", hash = "sha256:dafc32401712f5b21c12dc3d05ba887a91ad156d9b49a7662279f9fd90526fb2", size = 26742, upload-time = "2026-08-17T08:34:11.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/ec/6b368a10e9c2e8b1b394c69b96ac213ae66e8c4895e0baa1ffaf7178fd32/alibabacloud_tea_openapi-0.4.5-py3-none-any.whl", hash = "sha256:338979095c7beda80a5b413c31262892cafdc12069dde4ce4fc2e4f7ce0fc609", size = 33333, upload-time = "2026-07-14T13:15:38.365Z" },
{ url = "https://files.pythonhosted.org/packages/35/00/2f534f5884e5f299d9cb3a1e8be2def8071bc6a6e2a192ba4ff2a8cd5e02/alibabacloud_tea_openapi-0.4.6-py3-none-any.whl", hash = "sha256:c9e1727b9fb2936f487d050fc3590c99f9f2065256dc3a927e5b61f414674ed6", size = 33448, upload-time = "2026-08-17T08:34:10.472Z" },
]
[[package]]
@@ -4426,7 +4426,7 @@ wheels = [
[[package]]
name = "oci"
version = "2.183.0"
version = "2.184.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -4439,9 +4439,9 @@ dependencies = [
{ name = "pytz" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/2a/77bd6cbf1c69b2f368fe3d6462d84369b0cba15e37ce713cdc08d459b95a/oci-2.183.0.tar.gz", hash = "sha256:ff572ef5f2030a788796bb509d257e6a41c6510ef9b4b6a75a079efd06e533ce", size = 17759723, upload-time = "2026-07-28T06:02:29.76Z" }
sdist = { url = "https://files.pythonhosted.org/packages/74/2d/fa5368cfabb868f4111c6978e8b5f66aa3a55076c40c1a59ac3081b0227b/oci-2.184.1.tar.gz", hash = "sha256:617dad69caf8dd6e521d224dbc3e8a8bc289906943a0214fd2c3419094e26435", size = 17990631, upload-time = "2026-08-11T11:01:26.194Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/de/8574b3e527996a099d196e87794a4652d91a0c3185fcc7fdbb5649b75a8a/oci-2.183.0-py3-none-any.whl", hash = "sha256:bd789c98a94d7c5ea08c20d11dcf68c9cd1ad479b134727d80a930b84387070b", size = 36133501, upload-time = "2026-07-28T06:02:18.239Z" },
{ url = "https://files.pythonhosted.org/packages/5f/63/5ae22e42aaf96a5da74dc2b9de449c78b4d7418cce621d5da723b3e49f32/oci-2.184.1-py3-none-any.whl", hash = "sha256:bd814e38a70da2190e721937455a08689ab13c0750bd2ef8dd0c98b2dc5a38ea", size = 36628063, upload-time = "2026-08-11T11:01:18.178Z" },
]
[[package]]
@@ -4835,8 +4835,8 @@ wheels = [
[[package]]
name = "prowler"
version = "5.38.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#b3d174d0c1eb202ed7cb9a9daf0500683f4443be" }
version = "5.41.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#f05a490cd74a2c0f11a5d66d8ce29d03fa5c64a2" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4928,14 +4928,17 @@ dependencies = [
{ name = "stackit-iaas" },
{ name = "stackit-objectstorage" },
{ name = "stackit-resourcemanager" },
{ name = "stackit-ske" },
{ name = "tabulate" },
{ name = "truststore" },
{ name = "tzlocal" },
{ name = "uuid6" },
{ name = "zstandard" },
]
[[package]]
name = "prowler-api"
version = "1.40.0"
version = "1.42.0"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -5040,7 +5043,7 @@ requires-dist = [
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
{ name = "sentry-sdk", extras = ["django"], specifier = "==2.56.0" },
{ name = "sqlparse", specifier = "==0.5.5" },
{ name = "sqlparse", specifier = "==0.6.0" },
{ name = "uuid6", specifier = "==2024.7.10" },
{ name = "uvicorn-worker", specifier = "==0.4.0" },
{ name = "uvloop", specifier = "==0.22.1" },
@@ -5426,15 +5429,15 @@ wheels = [
[[package]]
name = "pyopenssl"
version = "26.2.0"
version = "26.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
{ url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" },
]
[[package]]
@@ -6049,11 +6052,11 @@ wheels = [
[[package]]
name = "sqlparse"
version = "0.5.5"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
{ url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
]
[[package]]
@@ -6117,6 +6120,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/9c/38a74d0f7a89b4320f6d2366fb660638bda8860daa08748b12c713d84381/stackit_resourcemanager-0.8.0-py3-none-any.whl", hash = "sha256:dd04bb8353d041a137c4dcba190beabded7acfaff1bc98b218fce20a99389ebc", size = 81288, upload-time = "2026-05-13T09:43:07.81Z" },
]
[[package]]
name = "stackit-ske"
version = "1.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dateutil" },
{ name = "requests" },
{ name = "stackit-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/9e/df3ad585cb96d028354f4253568e9879d81bb9395d5ebfa268fa9350e2df/stackit_ske-1.12.0.tar.gz", hash = "sha256:62814279f3b7fb2387648f92d14453a8905ad60115c07579f2741ddb7d1fcc94", size = 37239, upload-time = "2026-06-30T11:18:49.39Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/37/dc54fb7185a2d4da37308322ea1a7b992312030b2e37262de4eb4003f5c7/stackit_ske-1.12.0-py3-none-any.whl", hash = "sha256:45bd8084d87f14f818b3d7e824450248c8784ed204ca1b2dc108f491dcbdb1a3", size = 93142, upload-time = "2026-06-30T11:18:48.233Z" },
]
[[package]]
name = "statsd"
version = "4.0.1"
@@ -6225,6 +6243,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]]
name = "truststore"
version = "0.10.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
]
[[package]]
name = "typer"
version = "0.21.1"
@@ -6622,40 +6649,70 @@ wheels = [
]
[[package]]
name = "zstd"
version = "1.5.7.3"
name = "zstandard"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/62/b9c075ad664e7c4cbb3d8d2be7c246506abe1bc7f778eb58d260ef9538c8/zstd-1.5.7.3.tar.gz", hash = "sha256:403e5205f4ac04b92e6b0cda654be2f51de268228a0db0067bc087faacf2f495", size = 672559, upload-time = "2026-01-08T16:24:43.361Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/0d/8c89c0d010b58c21a7865a239790bb1c6822029c053b1ded858d6b573e3a/zstd-1.5.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a3c1781a24e2ced2c0ddee11d45b1f04018b03615eeb622a62eca4d56d3358a", size = 267641, upload-time = "2026-01-08T16:30:50.812Z" },
{ url = "https://files.pythonhosted.org/packages/a3/6d/155d8c344d96eca2a5a003a5ddd63373a5f13591fd5cf2b9490250d6805a/zstd-1.5.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c7c81056362b60a04baa34632e713d596662a860ec34efd8e9b109c10e6ec7", size = 230962, upload-time = "2026-01-08T16:30:49.155Z" },
{ url = "https://files.pythonhosted.org/packages/c8/c7/ab93916a26eb58cd501ad701974c31b4bc67a7f6abd6c24bef8fe4d7649b/zstd-1.5.7.3-cp311-cp311-manylinux_2_14_x86_64.whl", hash = "sha256:e564f34a55effc7d654eb293468edc80b64d476b0f899f82760ecd8323223ff5", size = 304166, upload-time = "2026-01-10T11:17:45.697Z" },
{ url = "https://files.pythonhosted.org/packages/c2/54/27a7040a360019a4602343e3c98c0c0a140f382186002c01e1992fd21837/zstd-1.5.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:fbc49a57188184931d5e3c9f1133cad7eea5a370a9e9418fb8122d58c14340a5", size = 1540288, upload-time = "2026-01-08T17:50:26.913Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/4a4d4edd1b2e809e0ebbb16000404bdcc9a09743c04ee1661442c9581b75/zstd-1.5.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d121d3e63722819e1fe5effbcd9628d8a7cfea0cddabcc5bb37ea861a6a83424", size = 1619134, upload-time = "2026-01-08T17:50:32.324Z" },
{ url = "https://files.pythonhosted.org/packages/31/6b/cd6f0a7f4f0d98e4110aa77763cf3e85f594d983ea9ca3d64cc0cee10684/zstd-1.5.7.3-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:621f2e7ca8e9eb52a83eb9c91ec3cd283d87591bf75cc658de486b65f44742c7", size = 300166, upload-time = "2026-01-10T11:12:27.938Z" },
{ url = "https://files.pythonhosted.org/packages/05/3f/c717e0d15127d04b7fa58ba9b4c56e8b88b803048b9766cd9d158dbb22ea/zstd-1.5.7.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:c1950fcae690ba32d0f31702b335c548fb42547821565925e48576afdad774a5", size = 1525776, upload-time = "2026-01-08T17:50:35.518Z" },
{ url = "https://files.pythonhosted.org/packages/3e/a2/1813cd787d1a2f9ab8e8a90d28dcbc8e8098997dd04de38897ea8e75dd08/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bac4f0d03da69115878bedbfa03c4a3f64364e8396b432028c4ce0f05141a0fb", size = 2096057, upload-time = "2026-01-08T17:50:33.984Z" },
{ url = "https://files.pythonhosted.org/packages/36/ce/f5a3c7c12de458dd9ce15c484d627fe5412b60c155da23dacb5fcf08d9d5/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:da0ab134b7fd28023dedf013751ca850de300a090eb11f689d2a1c178c87d9dc", size = 2132659, upload-time = "2026-01-08T17:50:29.534Z" },
{ url = "https://files.pythonhosted.org/packages/f1/66/151f9546498bfd8971a0b6ad67d87c26d7a0df17d57f724da674f3778666/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b9923175842ee8f7602ec9cc578f5fc396896f0e8460d3ac9a5adc3cea77244e", size = 2124811, upload-time = "2026-01-08T17:50:37.612Z" },
{ url = "https://files.pythonhosted.org/packages/6a/34/4d2dbb36cb2373d3f115c047cb901b64f89de0703d10779da39de9453812/zstd-1.5.7.3-cp311-cp311-win32.whl", hash = "sha256:0612b604948d7b58aecc6788c7ceb53c5f21d94a155bb6ea9bd0f54ffa43725d", size = 150363, upload-time = "2026-01-08T17:11:02.392Z" },
{ url = "https://files.pythonhosted.org/packages/d9/de/f53687e0dd8c0d0ebfaed9ae88f6a96a1a0388ae7424b469e74bb17ac57d/zstd-1.5.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:5b7f8c81b2bd3b62c0345242247d484cafa4b518d59d18619813d9225af5c5c3", size = 167577, upload-time = "2026-01-08T17:11:03.356Z" },
{ url = "https://files.pythonhosted.org/packages/f2/58/d4a6a902e229e953ed273fe9b78587ed31f57567aa68d3e34af6056e42af/zstd-1.5.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:ea112e3acd9e1765adca35df7b54ac75b36194290f64ea03a3a59664209c8527", size = 157238, upload-time = "2026-01-08T16:36:06.25Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ed/5a3bf2e29dc56d4cc7619929bb51f0c758de6d02967cc73c5d8755a862c0/zstd-1.5.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01a39efb0eeab7cc45cb308618233b624b0840d5e16dcf85456b6cca0592f203", size = 268124, upload-time = "2026-01-08T16:29:57.091Z" },
{ url = "https://files.pythonhosted.org/packages/e2/1d/efc2074ac90af938e78f2ed4004639fe24f294d9086c5280f8d9a02b9897/zstd-1.5.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a8e8838cf35fa3987bfe1958584cc22e1797efce8e155a63544b4144fc671f8", size = 230988, upload-time = "2026-01-08T16:29:55.604Z" },
{ url = "https://files.pythonhosted.org/packages/2a/52/178393b8d70e23fba67f42dfce4663e4e8a30867110168beb490a36d4639/zstd-1.5.7.3-cp312-cp312-manylinux_2_14_i686.whl", hash = "sha256:f3920ac1d1cc7e9f252f3e29f217fe3cd36f2191bb3dbcae826c29e189b7ad54", size = 300207, upload-time = "2026-01-10T11:26:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/8dcd86a2efb2ed3f9dae39545a05d3c7ed26c7678330786ce4a44cd8b099/zstd-1.5.7.3-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:143f9062953fb5590cbd47c1040d357336742c79696bf90b6d5b835279a68304", size = 304154, upload-time = "2026-01-10T11:17:40.91Z" },
{ url = "https://files.pythonhosted.org/packages/6f/ce/0c96905ab01ffe0e53a3cec8132123b82db26bd583a71608029bcc789ebc/zstd-1.5.7.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d1fd8647e47e1f21b345e192f1a279e925678c23dad8236b547d04456cd699", size = 2162222, upload-time = "2026-01-08T18:02:22.762Z" },
{ url = "https://files.pythonhosted.org/packages/11/c4/db4807d6a68b4628c74fd379de7e3c67ec34f19a2a80ac246b3837cde6cb/zstd-1.5.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1538db419afa62773cf534fc7f3009ff59ecf55ecee4e889587ac2ef0010ed8", size = 2201732, upload-time = "2026-01-08T18:02:20.835Z" },
{ url = "https://files.pythonhosted.org/packages/c5/99/c19a3c0f5580ff9c33a74f06d98d6060ed1fa6bd09b55aed9be852ec191f/zstd-1.5.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5efd16adb092e2a547a7d51cfdaf6fd5680528227684c5bafc7669ab4a55f41", size = 2096459, upload-time = "2026-01-08T18:02:25.336Z" },
{ url = "https://files.pythonhosted.org/packages/23/fd/02eac30419475dbe50212c119043a2d0698a0cbc756da85fd3fd9abddf42/zstd-1.5.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:39b3438e64637d80a5b1860526903b92020acb9bae9ceb5adffd9838c1441328", size = 2125442, upload-time = "2026-01-08T18:02:17.715Z" },
{ url = "https://files.pythonhosted.org/packages/bb/43/3a16ff0a8c913bb9825379db1bd533c75c57c2d2f31dd9111aa9b53711f4/zstd-1.5.7.3-cp312-cp312-win32.whl", hash = "sha256:cbf48c53461e224ffc2490cfe5120a1ff40d14c84d2b512c6d6d99fc91685cf3", size = 150367, upload-time = "2026-01-08T17:03:40.178Z" },
{ url = "https://files.pythonhosted.org/packages/46/83/b85875d7428e63dfa9247e41d17fac611443c774f7892f8643bd4164a6b2/zstd-1.5.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:943a189910f2fea997462e3e4d7fbf727a06d231ef801ebee557b1c87568981c", size = 167604, upload-time = "2026-01-08T17:03:41.355Z" },
{ url = "https://files.pythonhosted.org/packages/37/42/cf291e26804de2f55500cdac93f5e9fa6267cf315def8aa402529bae3a87/zstd-1.5.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:85c4d508f8109afa7c51c4960626c3325af2cf1e442c6c36ebfea15d04757e3f", size = 157241, upload-time = "2026-01-08T16:47:34.615Z" },
{ url = "https://files.pythonhosted.org/packages/04/b8/d13d584867d5eb1bc607877a870858e02a256d4706a4274e475413a000aa/zstd-1.5.7.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:76c49ea969bc08389ea59155cea7c5dea224522ffc62f443f3c0a915f5fd184d", size = 260025, upload-time = "2026-01-08T16:57:45.739Z" },
{ url = "https://files.pythonhosted.org/packages/16/a1/1e5faf75bedfd2bfccfb83e18736b115bed6e348504bd21800cd8f30dcea/zstd-1.5.7.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b1a638ff3dfce8f4cb1203c662fb5606dd99b4a62c5ddc4c406d2d1326bcfdd", size = 221038, upload-time = "2026-01-08T17:16:32.005Z" },
{ url = "https://files.pythonhosted.org/packages/b7/2c/0fe74d8b2029eef8000bc71aac5b3e5b55d00581238711cf627814183ea3/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5e96a5cb100a0edc162935227f2d9784b1031ce4a8a83e96e66eae2673c10143", size = 326792, upload-time = "2026-01-08T16:57:35.631Z" },
{ url = "https://files.pythonhosted.org/packages/96/e0/2c7f081f3524f872128ff31bea2acb6b21cb1dacccef920eb6a1a77a87c6/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bda0bbf3a9553720cd33f1f85940a259656c7ffba4be717ff82b7f062052188", size = 322283, upload-time = "2026-01-08T16:57:36.759Z" },
{ url = "https://files.pythonhosted.org/packages/c9/a7/3bebfcc18d66b90bc7b506a61b2ff4af5ee1b0b16e784ea644afa06241c5/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac36e4022422f6e49b3f07bdbb8a964fd348223d3dc9c82ad5398a4f0432a719", size = 311553, upload-time = "2026-01-08T16:57:38.465Z" },
{ url = "https://files.pythonhosted.org/packages/41/75/8a791cae2c98e5e44a158e15db50d21b7ec0b37aeaffa68d151bc8ffb6d6/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:fa4d760a220541b18ce732a3a2cf7547ea05afc76d05b3b39edebfeb721f6079", size = 317071, upload-time = "2026-01-08T16:36:07.47Z" },
{ url = "https://files.pythonhosted.org/packages/2f/25/b6624e6b08d515242154436c9d06fb20b790d300ac82e84f3c4c133e25e1/zstd-1.5.7.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a69e60146bf8aaa6a0e6c9a94a7c5f3133d68091e2e5c5a3c5ababf71fd5ec7a", size = 167654, upload-time = "2026-01-08T17:00:56.667Z" },
{ url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
{ url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
{ url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
{ url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
{ url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
{ url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
{ url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
{ url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
{ url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
{ url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
{ url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
{ url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
{ url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
{ url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
{ url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
{ url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
{ url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
{ url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
{ url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
{ url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
{ url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
{ url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
{ url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
{ url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
{ url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
{ url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
{ url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
{ url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
{ url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
]
[[package]]
name = "zstd"
version = "1.5.7.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/78/9a476e09c825304df47b98be80d1ffe223733b03550af71325415028f615/zstd-1.5.7.2.tar.gz", hash = "sha256:6d8684c69009be49e1b18ec251a5eb0d7e24f93624990a8a124a1da66a92fc8a", size = 670481, upload-time = "2025-06-23T12:36:08.131Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/2a/0885f6f1921ec1ef4a8f8ab29ab0a335cc867abe4c7aaa4e5031435a32a5/zstd-1.5.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f799c1e9900ad77e7a3d994b9b5146d7cfd1cbd1b61c3db53a697bf21ffcc57b", size = 269702, upload-time = "2025-06-23T12:50:11.695Z" },
{ url = "https://files.pythonhosted.org/packages/05/e6/629cf6b77e47fc7149f5724fb4853c48edcdeb10d8c64e391d7026cb10e1/zstd-1.5.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ff4c667f29101566a7b71f06bbd677a63192818396003354131f586383db042", size = 228145, upload-time = "2025-06-23T12:50:10.411Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b8/9ddefd4670bfe9328ca6657ad335eb8d9c657466247e234a579818b6b0b9/zstd-1.5.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8526a32fa9f67b07fd09e62474e345f8ca1daf3e37a41137643d45bd1bc90773", size = 1536530, upload-time = "2025-06-23T13:51:38.853Z" },
{ url = "https://files.pythonhosted.org/packages/d1/6a/1bb836c18760dc1e28ca7a9706016e482ebdea633b980d8505dbb65e18f8/zstd-1.5.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2cec2472760d48a7a3445beaba509d3f7850e200fed65db15a1a66e315baec6a", size = 1616141, upload-time = "2025-06-23T13:51:34.152Z" },
{ url = "https://files.pythonhosted.org/packages/b5/7a/bb6c6e2cb2a066e347dc27d45d5205058b69d6c8b8d4ae2ee7d6b91c64a5/zstd-1.5.7.2-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:a200c479ee1bb661bc45518e016a1fdc215a1d8f7e4bf6c7de0af254976cfdf6", size = 322188, upload-time = "2025-06-23T13:01:48.704Z" },
{ url = "https://files.pythonhosted.org/packages/5a/4f/cf0669c8a89fdcc91814bf92bd05cc363d5d12a79b656418c0add6f2d266/zstd-1.5.7.2-cp311-cp311-manylinux_2_4_x86_64.whl", hash = "sha256:f5d159e57a13147aa8293c0f14803a75e9039fd8afdf6cf1c8c2289fb4d2333a", size = 302736, upload-time = "2025-06-23T13:05:33.649Z" },
{ url = "https://files.pythonhosted.org/packages/be/bc/e5f8b7f61826323e39e099db1eb5c0e09b18315df1b1ff778f7ae9aadcac/zstd-1.5.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:7206934a2bd390080e972a1fed5a897e184dfd71dbb54e978dc11c6b295e1806", size = 1522687, upload-time = "2025-06-23T13:51:35.494Z" },
{ url = "https://files.pythonhosted.org/packages/d5/8c/7660a949a020ac9d02b3166a25dd1c12144572d77b11ae92a31d341016da/zstd-1.5.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e0027b20f296d1c9a8e85b8436834cf46560240a29d623aa8eaa8911832eb58", size = 2098794, upload-time = "2025-06-23T13:51:37.219Z" },
{ url = "https://files.pythonhosted.org/packages/bc/b2/730c811a78d670104d40c7f08cc8092577cdff870cba42b3158f20fceb57/zstd-1.5.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d6b17e5581dd1a13437079bd62838d2635db8eb8aca9c0e9251faa5d4d40a6d7", size = 2112266, upload-time = "2025-06-23T13:51:31.258Z" },
{ url = "https://files.pythonhosted.org/packages/44/74/2c16e1632094db36c8920d4c13b8e2e843024d548ae26888c2d22af6a676/zstd-1.5.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b13285c99cc710f60dd270785ec75233018870a1831f5655d862745470a0ca29", size = 2109465, upload-time = "2025-06-23T13:51:32.884Z" },
{ url = "https://files.pythonhosted.org/packages/58/6e/b9c9a834769d96cab2122da1be8c8c700d3f76be796d2b7516e85d2eca0e/zstd-1.5.7.2-cp311-cp311-win32.whl", hash = "sha256:cdb5ec80da299f63f8aeccec0bff3247e96252d4c8442876363ff1b438d8049b", size = 149448, upload-time = "2025-06-23T13:06:21.144Z" },
{ url = "https://files.pythonhosted.org/packages/47/b7/fc22ad6292a32d7676ab815de3a23573beac3679e8abd9914288d1496ceb/zstd-1.5.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:4f6861c8edceb25fda37cdaf422fc5f15dcc88ced37c6a5b3c9011eda51aa218", size = 166592, upload-time = "2025-06-23T13:06:22.126Z" },
{ url = "https://files.pythonhosted.org/packages/45/14/096bb77f3e5ef525b452cd6294da33de7f8a8c9647ba78293378fbb0a7ce/zstd-1.5.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ebe3e60dbace52525fa7aa604479e231dc3e4fcc76d0b4c54d8abce5e58734", size = 269408, upload-time = "2025-06-23T13:11:46.492Z" },
{ url = "https://files.pythonhosted.org/packages/08/b8/2bc2590a34c733ea0570f366e6ad7d889d05c7825bd3ccab01f36ece71c6/zstd-1.5.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef201b6f7d3a6751d85cc52f9e6198d4d870e83d490172016b64a6dd654a9583", size = 228188, upload-time = "2025-06-23T13:11:47.539Z" },
{ url = "https://files.pythonhosted.org/packages/b7/80/6252de3a70cfd7767718ad476893f1c7dc129f942cc7ed0322e3137c03d9/zstd-1.5.7.2-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:ac7bdfedda51b1fcdcf0ab69267d01256fc97ddf666ce894fde0fae9f3630eac", size = 302720, upload-time = "2025-06-23T12:40:11.522Z" },
{ url = "https://files.pythonhosted.org/packages/af/b6/af908387814b99172d3aea6aeb24b19583aadfa45f6021e5e2a0d6d8e99a/zstd-1.5.7.2-cp312-cp312-manylinux_2_4_i686.whl", hash = "sha256:b835405cc4080b378e45029f2fe500e408d1eaedfba7dd7402aba27af16955f9", size = 322237, upload-time = "2025-06-23T13:17:35.482Z" },
{ url = "https://files.pythonhosted.org/packages/ed/d7/ab9142e002a7eaa451cb4bb37a74c390c489ba8ae75ade543840496eda04/zstd-1.5.7.2-cp312-cp312-win32.whl", hash = "sha256:e4cf97bb97ed6dbb62d139d68fd42fa1af51fd26fd178c501f7b62040e897c50", size = 149453, upload-time = "2025-06-23T13:13:02.786Z" },
{ url = "https://files.pythonhosted.org/packages/3e/c7/c182ea7bc283f591e3f3c5f0f239e7a92c9bc1f626642ae2c4dfbe51d6f2/zstd-1.5.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:55e2edc4560a5cf8ee9908595e90a15b1f47536ea9aad4b2889f0e6165890a38", size = 166628, upload-time = "2025-06-23T13:13:03.745Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c9/a6495a7bf168a78f0a0c01d61d830ebfb401315a64fd1ae8d725c458114c/zstd-1.5.7.2-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:5fb2ff5718fe89181223c23ce7308bd0b4a427239379e2566294da805d8df68a", size = 315542, upload-time = "2025-06-23T12:39:27.598Z" },
]
+334 -1
View File
@@ -4,6 +4,339 @@ description: "New features and improvements in each Prowler release"
rss: true
---
<Update label="v5.41.0" description="September 2, 2026">
### 📥 Scans — Import Findings from the Browser
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Findings produced outside the platform, by the Prowler CLI or a CI pipeline, can now be brought into the app without leaving the browser. The Scans page gains an "Import Findings" dialog that takes a Prowler `.ocsf.json` report by drag-and-drop or file picker, hands it to the ingestion API and tracks the job to completion, reporting how many records were processed and how many were invalid. Files that are not a `.ocsf.json` report, or are empty, are refused before any upload starts, and a rejected upload or a failed status poll can be retried in place. The dialog is available to roles holding the Manage Ingestions permission.
![Import Findings button on the Scans page](/images/prowler-app/import-findings/import-findings-button.png)
![Import findings dialog with the drag-and-drop area](/images/prowler-app/import-findings/import-findings-dialog.png)
Read more in the [Import Findings documentation](https://docs.prowler.com/user-guide/tutorials/prowler-import-findings#using-the-ui).
### 🎫 Jira Integration — Finding Reference in Every Issue
Every Jira issue created from a finding now carries a stable reference back to it. Issues are labeled `prowler`, `prowler-<provider>`, `prowler-<severity>`, `prowler-<check-id>` and `prowler-finding-<finding-uid>`, so they can be filtered, searched with JQL or matched by automation; labels are sanitized to Jira's limits so a long or unusual value never blocks issue creation. The issue also links back to the finding in Prowler, filtered by its UID so the link keeps working after later scans, and names the Prowler organization that sent it. Prowler Cloud always includes the link; Prowler Local Server enables it by setting `DJANGO_UI_BASE_URL` in the API environment.
Read more in the [Jira integration documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-jira-integration).
### 📚 Compliance — CIS Google Workspace Foundations Benchmark v1.4.0
Prowler now ships the CIS Google Workspace Foundations Benchmark v1.4.0. Alongside the new framework, the Google Workspace checks mapped to CIS were reworked to evaluate the benchmark's full audit procedure instead of a single condition, so Gmail spoofing actions, 2-Step Verification, password expiration and alert severity left on Google's defaults no longer pass. Expect new `FAIL` findings on domains that rely on those defaults. Three accuracy fixes also land:
- `security_2sv_enforced` and `security_2sv_hardware_keys_admins` report `MANUAL` instead of judging domain-wide values that a group or a sub-organizational unit overrides; a domain-wide failure is still reported as such, with the override noted.
- `rules_*_alert_configured` no longer passes a rule whose delivery to the alert center is disabled.
- `security_password_policy_strong` no longer fails a domain that never touched the password strength setting, since Google enforces strong passwords by default.
`security_login_challenges_configured` was unmapped from CIS Google Workspace requirement 4.1.4.1 (Post-SSO verification) and `security_2sv_enforced` from CISA SCuBA `GWS.COMMONCONTROLS.1.1` (phishing-resistant MFA), because neither check can prove what those requirements ask for.
Read more in the [Compliance documentation](https://docs.prowler.com/user-guide/compliance/tutorials/compliance).
### 🔍 Checks
Ten new AWS checks land in this release, eight of them contributed by @tamg-aws. Thank you!
#### Amazon Bedrock AgentCore
- `iam_policy_passrole_to_bedrock_agentcore_restricted` flags customer-managed IAM policies that allow `iam:PassRole` over every role where the passed role can reach Bedrock AgentCore, so any principal holding the policy could run agent code under any role in the account.
- `iam_policy_no_agentcore_workload_access_token_wildcard` flags customer-managed IAM policies that allow `bedrock-agentcore:GetWorkloadAccessToken`, `GetWorkloadAccessTokenForJWT` or `GetWorkloadAccessTokenForUserId` on resources reaching workload identities other than the caller's own.
- `cloudwatch_log_group_agentcore_data_protection_policy_enabled` verifies that Bedrock AgentCore log groups mask sensitive data with a CloudWatch Logs data protection policy. The log group prefixes are configurable through `agentcore_log_group_name_prefixes` in `config.yaml`.
#### Amazon GuardDuty
- `guardduty_runtime_monitoring_enabled` flags detectors without unified Runtime Monitoring, the only feature that covers Amazon EC2 instances and Amazon ECS on AWS Fargate tasks in addition to Amazon EKS.
- `guardduty_ai_protection_enabled` flags detectors without AI Protection, which analyzes CloudTrail data events from Amazon Bedrock, Amazon Bedrock AgentCore and Amazon SageMaker AI. A detector that does not report the feature is `MANUAL` rather than `FAIL`.
`guardduty_eks_runtime_monitoring_enabled` no longer reports `FAIL` for detectors that use unified Runtime Monitoring, which is mutually exclusive with `EKS_RUNTIME_MONITORING` and already covers Amazon EKS.
#### Amazon ECR and EKS
- `ecr_registry_enhanced_scanning_enabled` verifies that the ECR registry scan type is enhanced (Amazon Inspector, covering programming language packages and continuous rescanning) instead of basic, reporting `MANUAL` when the registry scanning configuration cannot be read.
- `eks_cluster_vpc_cni_network_policy_enforced` flags EKS clusters whose Amazon VPC CNI managed add-on does not enable Kubernetes network policy enforcement, reporting `MANUAL` where the EKS API cannot show the setting.
#### AWS IAM, Elastic Beanstalk and MemoryDB
- `iam_role_service_trust_restricts_source_to_account` flags IAM roles whose trust policy lets an AWS service principal assume the role without confining the request to a specific source account, including trust policies that `iam_role_cross_service_confused_deputy_prevention` does not evaluate.
- `elasticbeanstalk_environment_no_secrets_in_configuration` scans the option settings of every Elastic Beanstalk environment for hardcoded secrets. Thanks to @haneul-24!
- `memorydb_cluster_in_transit_encryption_enabled` verifies that MemoryDB clusters have in-transit encryption (TLS) enabled. Thanks to @UTKARSH698!
Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
### 🐳 Image Provider — On-Premises Registries
Scanning registries that live on private networks is now supported end to end. `PROWLER_IMAGE_PROVIDER_ALLOWED_PRIVATE_NETWORKS` takes a comma-separated list of IPs and CIDRs the provider may reach, while every other non-public address, including link-local and loopback, stays blocked by the SSRF guard. Authentication negotiation is also more resilient: the provider falls back to Basic when a registry such as Harbor rejects the negotiated bearer token, and switches to a bearer token when the server answers a Basic or anonymous request with a Bearer challenge. `--registry-insecure` now propagates to Trivy through `TRIVY_INSECURE`, so images behind self-signed certificates can be pulled and scanned, not just enumerated. The flag now disables certificate validation for the image pull too, so keep it for trusted internal registries only.
Registry scans also skip non-image OCI artifacts (Helm charts, cosign signatures, SBOM attestations), no longer abort the whole scan when Trivy fails on a single image, and enumerate repositories in parallel instead of one request at a time.
Read more in the [Image provider documentation](https://docs.prowler.com/user-guide/providers/image/getting-started-image#on-premises-registries-and-private-networks).
### 🛠️ Prowler MCP Server — Tool Failures Reported as Errors
Prowler Local Server tools now report a failure as an MCP tool execution error (`isError: true`, with the explanation in `content`) instead of a successful result carrying an `{"error": ...}` object, which clients and models read as a success. The Prowler Documentation and Prowler Hub tools follow the same rule: `prowler_docs_search` no longer reports a failed search as zero matches, `prowler_docs_get_document` no longer reports a failed fetch as a missing page, and `prowler_hub_get_check_code` and `prowler_hub_get_check_fixer` now name the provider a check ID actually belongs to instead of reporting it as nonexistent. `prowler_get_compliance_framework_state_details` also rejects a call that passes both `scan_id` and `provider_id` instead of silently ignoring the provider.
Read more in the [Prowler MCP documentation](https://docs.prowler.com/getting-started/products/prowler-mcp).
### 🙌 External Contributors
Thank you to our community contributors for this release!
- @tamg-aws: GuardDuty unified Runtime Monitoring and AI Protection checks ([#12564](https://github.com/prowler-cloud/prowler/pull/12564)), EKS VPC CNI network policy check ([#12661](https://github.com/prowler-cloud/prowler/pull/12661)), ECR enhanced scanning check ([#12660](https://github.com/prowler-cloud/prowler/pull/12660)), Bedrock AgentCore IAM and service trust checks ([#12664](https://github.com/prowler-cloud/prowler/pull/12664)), AgentCore log group data protection check ([#12662](https://github.com/prowler-cloud/prowler/pull/12662)), and fixes to ECR scan frequency ([#12560](https://github.com/prowler-cloud/prowler/pull/12560)), CloudWatch metric filters ([#12561](https://github.com/prowler-cloud/prowler/pull/12561)) and SageMaker direct internet access ([#12659](https://github.com/prowler-cloud/prowler/pull/12659))
- @haneul-24: AWS `elasticbeanstalk_environment_no_secrets_in_configuration` check ([#12378](https://github.com/prowler-cloud/prowler/pull/12378))
- @UTKARSH698: AWS `memorydb_cluster_in_transit_encryption_enabled` check ([#12246](https://github.com/prowler-cloud/prowler/pull/12246))
- @ye11oc4t: GitHub repository discovery pagination for unscoped scans ([#12460](https://github.com/prowler-cloud/prowler/pull/12460))
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.41.0) for the complete list of changes.
</Update>
<Update label="v5.40.0" description="August 28, 2026">
### 💬 Slack Integration — Alert Channel Destinations
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Alerts can now reach Slack. Connect a Slack workspace from the Integrations page, authorize one or several destination channels (the connection check confirms each channel with a one-time message and names any channel Slack refuses), and pick those channels in the alert modal's "Destination channels" selector, next to the "Recipients" selector for email. The alerts list summarizes both in a single "Destinations" column, showing a rule's email recipients and Slack channels at a glance. Disconnecting the workspace and recovering from revoked credentials are handled from the same page.
![Connected Slack workspace on the Integrations page](/images/prowler-app/slack/connected-workspace.png)
![Alert rule with Slack channel destinations](/images/prowler-app/alerts/create-alert-modal.png)
Read more in the [Slack integration documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-slack-integration) and the [Alerts documentation](https://docs.prowler.com/user-guide/tutorials/prowler-alerts).
### 🤖 Lighthouse AI — Answer Feedback
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Every Lighthouse AI answer can now be rated with a thumbs up or thumbs down, with an optional field to describe what worked or what did not. Feedback is collected per answer, directly in the chat, and tells the team where Lighthouse should improve next.
Read more in the [Lighthouse AI documentation](https://docs.prowler.com/getting-started/products/prowler-cloud-lighthouse).
### 📥 Providers — Imported Findings Indicator
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Providers whose findings were imported with the Prowler CLI now show an "Imported provider" indicator next to their connection status in the providers table. In accounts that mix connected providers with Import Findings uploads, the table now tells them apart at a glance.
![Provider row with the Imported provider indicator and its tooltip](/images/changelog/v5.40.0-imported-provider-indicator.png)
Read more in the [Import Findings documentation](https://docs.prowler.com/user-guide/tutorials/prowler-import-findings).
### 📚 Compliance — NCSC Cyber Essentials 3.3
Cyber Essentials is the UK National Cyber Security Centre (NCSC) scheme certifying the baseline technical controls an organization must implement, and cloud services are explicitly in scope and cannot be excluded from an assessment. Prowler now includes NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026) as a universal framework, with its 28 requirements organized in the five control themes: Firewalls, Secure Configuration, Security Update Management, User Access Control, and Malware Protection.
Sixteen requirements map to Azure checks covering the controls the applicant organization owns under the shared responsibility model. The remaining twelve apply to end-user devices, on-premises network appliances, or organizational process, which cloud control-plane evidence cannot observe, so they are reported as Manual.
Contributed by @m-khan-97. Thank you!
Read more in the [Compliance documentation](https://docs.prowler.com/user-guide/compliance/tutorials/compliance).
### 🔍 Checks
Fifteen new checks land across seven providers in this release.
#### AWS
- `ecr_repository_image_no_secrets` scans the latest image of each ECR repository, both its configuration and its filesystem layers, for hardcoded secrets. Thanks to @esquaredsec!
- Four new Amazon Bedrock checks, thanks to @tamg-aws!
- `bedrock_guardrail_contextual_grounding_filter_enabled` verifies that guardrails enable both contextual grounding filters, blocking responses that are not supported by the retrieved source or do not answer the question asked.
- `bedrock_custom_model_encrypted_with_cmk` verifies that custom models are encrypted at rest with a customer-managed KMS key instead of an AWS-owned key the organization cannot audit, rotate, or revoke.
- `bedrock_knowledge_base_encrypted_with_cmk` verifies that each knowledge-base data source encrypts with a customer-managed KMS key the transient storage used while documents are chunked and embedded.
- `bedrock_agent_role_not_shared_across_agents` verifies that every agent has a dedicated execution role, so no agent inherits another's permissions.
- `rolesanywhere_profile_restricts_session_permissions` flags IAM Roles Anywhere profiles that reference an administrative role without scoping down the vended session with a session policy or managed policies.
Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
#### GCP
- `iam_workload_identity_pool_provider_attribute_condition` flags Workload Identity Federation providers that trust a multi-tenant issuer without an attribute condition restricting which external identities can impersonate federated principals.
Explore all GCP checks at [Prowler Hub](https://hub.prowler.com/check?provider=gcp).
#### GitHub
Three new checks harden GitHub Actions defaults, all contributed by @Edneam. Thank you!
- `organization_default_workflow_permissions_read_only` and `repository_default_workflow_permissions_read_only` verify that workflows get a read-only default `GITHUB_TOKEN` at the organization and repository level.
- `organization_actions_pull_request_approval_disabled` verifies that organizations prevent GitHub Actions from creating and approving pull requests.
Explore all GitHub checks at [Prowler Hub](https://hub.prowler.com/check?provider=github).
#### Microsoft 365
- `defender_domain_dmarc_records_published` checks that every Exchange Online domain publishes a DMARC record with an enforcing policy (`p=quarantine` or `p=reject`). Thanks to @Rishi943!
Explore all Microsoft 365 checks at [Prowler Hub](https://hub.prowler.com/check?provider=m365).
#### Alibaba Cloud
- `oss_bucket_versioning_enabled` verifies that OSS buckets keep versioning enabled, allowing recovery from accidental or malicious object overwrite and deletion. Thanks to @abidedavana!
- `oss_bucket_server_side_encryption_enabled` verifies that OSS buckets define a default server-side encryption rule, either AES256 or KMS. Thanks to @alexchen-sys!
OSS bucket logging, versioning, default encryption, and ACL configurations are also now read correctly from the Alibaba Cloud SDK, so the checks reading them no longer report every bucket as unconfigured.
Explore all Alibaba Cloud checks at [Prowler Hub](https://hub.prowler.com/check?provider=alibabacloud).
#### Huawei Cloud
- `vpc_security_group_open_egress` flags VPC security groups that allow open egress to the internet. Thanks to @tomitobio!
Explore all Huawei Cloud checks at [Prowler Hub](https://hub.prowler.com/check?provider=huaweicloud).
#### STACKIT
- `ske_cluster_no_public_endpoint` flags SKE clusters whose Kubernetes API endpoint is reachable from the whole internet, because the ACL extension is disabled or its allowed CIDR list contains `0.0.0.0/0` or `::/0`. Thanks to @johannes-engler-mw!
Explore all STACKIT checks at [Prowler Hub](https://hub.prowler.com/check?provider=stackit).
### 🔐 Security Updates
- The API and SDK container images upgrade OpenSSL to 3.5.7-1~deb13u2, patching ten high CVEs; the UI image upgrades `libcrypto3` and `libssl3` to 3.5.8-r0, patching seven high CVEs; the MCP Server image patches CVE-2026-14456 (OpenSSL), CVE-2026-11822, and CVE-2026-11824 (SQLite).
- `sqlparse` upgraded to 0.6.0 in the API, patching CVE-2026-54284, CVE-2026-59893, and CVE-2026-71491.
### 🙌 External Contributors
Thank you to our community contributors for this release!
- @Edneam: GitHub `organization_default_workflow_permissions_read_only` ([#12122](https://github.com/prowler-cloud/prowler/pull/12122)), `repository_default_workflow_permissions_read_only` ([#12143](https://github.com/prowler-cloud/prowler/pull/12143)), and `organization_actions_pull_request_approval_disabled` ([#12394](https://github.com/prowler-cloud/prowler/pull/12394)) checks
- @tamg-aws: four AWS Bedrock checks covering guardrail grounding, CMK encryption, and agent role isolation ([#12459](https://github.com/prowler-cloud/prowler/pull/12459))
- @esquaredsec: AWS `ecr_repository_image_no_secrets` check ([#12123](https://github.com/prowler-cloud/prowler/pull/12123))
- @Rishi943: Microsoft 365 `defender_domain_dmarc_records_published` check ([#11936](https://github.com/prowler-cloud/prowler/pull/11936))
- @abidedavana: Alibaba Cloud `oss_bucket_versioning_enabled` check ([#11913](https://github.com/prowler-cloud/prowler/pull/11913))
- @alexchen-sys: Alibaba Cloud `oss_bucket_server_side_encryption_enabled` check ([#11981](https://github.com/prowler-cloud/prowler/pull/11981))
- @tomitobio: Huawei Cloud `vpc_security_group_open_egress` check ([#12209](https://github.com/prowler-cloud/prowler/pull/12209))
- @johannes-engler-mw: STACKIT `ske_cluster_no_public_endpoint` check ([#11943](https://github.com/prowler-cloud/prowler/pull/11943))
- @gabrielfrdev: cluster name in Kubernetes compliance report outputs ([#12506](https://github.com/prowler-cloud/prowler/pull/12506))
- @jfgmesquita: AWS FSBP compliance mapping fix for IAM.9 and EKS.1 ([#12372](https://github.com/prowler-cloud/prowler/pull/12372))
- @hackertwinten: `ec2_securitygroup_not_used` no longer flags security groups held only by scaled-down AWS Batch compute environments ([#12458](https://github.com/prowler-cloud/prowler/pull/12458))
- @0xTaoZ: ECS task-definition checks no longer report PASS when `DescribeTaskDefinition` fails, shipped early in v5.39.1 ([#12217](https://github.com/prowler-cloud/prowler/pull/12217))
- @ye11oc4t: `ses_identity_not_publicly_accessible` now evaluates every identity authorization policy, shipped early in v5.39.1 ([#12464](https://github.com/prowler-cloud/prowler/pull/12464))
- @Zuhef: IaC provider raises typed exceptions instead of `sys.exit` when cloning the scanned repository or running Trivy fails ([#12227](https://github.com/prowler-cloud/prowler/pull/12227)), Kubernetes kubelet checks no longer disappear from the scan when a `kubelet-config` ConfigMap is broken ([#12225](https://github.com/prowler-cloud/prowler/pull/12225)), and the CLI `--slack` summary is sent for scans with no findings instead of failing with `ZeroDivisionError` ([#12229](https://github.com/prowler-cloud/prowler/pull/12229))
- @m-khan-97: NCSC Cyber Essentials 3.3 compliance framework with Azure provider coverage across the five Cyber Essentials themes ([#11588](https://github.com/prowler-cloud/prowler/pull/11588))
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.40.0) for the complete list of changes.
</Update>
<Update label="v5.39.0" description="August 13, 2026">
### 🤖 Lighthouse AI — Finding Skills
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Lighthouse AI now embeds a Skills menu on every finding, answering the questions an analyst actually asks. **Contextual Fix** produces the fix for the finding, **Triage Decision** judges whether it is real and closes it out when it is not, and **Systemic Scope** determines whether the problem is a one-off or everywhere. A free-form "Ask Lighthouse anything" prompt sits in the same menu, and each run shows its progress and offers follow-up actions such as creating a Jira issue or muting the finding.
![Lighthouse AI Skills menu on a finding resource](/images/changelog/v5.39.0-lighthouse-finding-skills.png)
Read more in the [Lighthouse AI documentation](https://docs.prowler.com/getting-started/products/prowler-cloud-lighthouse).
### ☁️ Azure Management Group Onboarding
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Azure subscriptions no longer onboard one at a time. Choose "Add Multiple Subscriptions With Azure Management Group" in the add-provider wizard, enter the Microsoft Entra tenant ID, and authenticate once with a single tenant-wide service principal: Prowler discovers the entire management-group hierarchy under the tenant root, lets you select the subscriptions to onboard, and creates their providers with the management-group structure preserved. Azure now matches the one-step onboarding that AWS Organizations and GCP organizations already have.
![Azure onboarding method selector with the Management Group option](/images/changelog/v5.39.0-azure-mg-selector.png)
Read more in the [Azure Management Groups documentation](https://docs.prowler.com/user-guide/tutorials/prowler-cloud-azure-management-groups).
### ✅ Findings Triage — Verify MANUAL Findings as PASS
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
Checks that require human judgment report `MANUAL` findings. For these findings, and only for them, the triage status selector now offers **Resolved**: choosing it asks for the required written evidence and verifies the finding as passing. The finding then reports an effective `PASS` while preserving the raw `MANUAL` scan result, across findings, finding groups, compliance reports, and scans, with the attestation's author, evidence, and validity always visible. Attestations expire automatically after 90 days, or as soon as a new scan reports a real failure, returning the finding to the review queue.
![Triage status selector offering Resolved on a MANUAL finding](/images/changelog/v5.39.0-manual-pass-selector.png)
![Manual Pass details showing evidence, author, and validity](/images/changelog/v5.39.0-manual-pass-details.png)
Read more in the [Findings Triage documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-findings-triage#verify-a-manual-finding-as-pass).
### ☁️ Prowler Cloud MCP — Organizations Management and Grouped Jira Dispatch
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
The hosted Prowler Cloud MCP server adds eight organization tools, so an agent can onboard and manage entire cloud organizations end to end: create the organization, discover its accounts, subscriptions, and projects, apply the selection, and manage the resulting providers. The tools cover AWS Organizations, GCP organizations, and Azure tenant root management groups, and they are available to Lighthouse AI.
`prowler_send_findings_to_jira` also gains Cloud-only dispatch capabilities: select failed findings by check IDs against the latest completed scan, and send them in grouped mode, one Jira work item per check listing up to 50 affected resources, with per-group error reporting.
Read more in the [Prowler MCP tools documentation](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools) and its [Jira operations reference](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools#jira-operations).
### 🕸️ Attack Paths — Grouped Graph with Outcome Destinations
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
The Attack Paths graph now reads from source to destination. Resources of the same class collapse into a single expandable node with a count, clicking reveals its members, and every path terminates in an explicit outcome node naming the destination impact: code execution, privilege escalation, public exposure, or resource inventory. The per-account hub node is gone, and the clicked resource stays highlighted while its findings are expanded.
![Attack Paths graph from the Internet to a public exposure outcome node](/images/changelog/v5.39.0-attack-paths-graph.png)
Explore the full Attack Paths query catalog at [Prowler Hub](https://hub.prowler.com/attack-paths).
Read more in the [Attack Paths documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-attack-paths).
### 📚 New Compliance Framework — CMMC 2.0
The Cybersecurity Maturity Model Certification (CMMC) is the certification the US Department of Defense requires from contractors and suppliers that handle federal contract data. Prowler now includes CMMC 2.0 as a universal framework with all 149 requirements defined by the CMMC Program rule (32 CFR Part 170), organized in its three levels:
- **Level 1 (Foundational):** 15 requirements for the basic safeguarding of Federal Contract Information, from FAR 52.204-21.
- **Level 2 (Advanced):** 110 requirements from NIST SP 800-171 Rev 2, protecting Controlled Unclassified Information.
- **Level 3 (Expert):** 24 enhanced requirements from NIST SP 800-172 for the most sensitive programs.
Requirements map to Prowler checks across AWS, Azure, GCP, Alibaba Cloud, Oracle Cloud, and Microsoft 365, so one framework reports the compliance posture of the whole estate.
Read more in the [Compliance documentation](https://docs.prowler.com/user-guide/compliance/tutorials/compliance).
### 🔍 Checks
#### Microsoft 365
Twenty new Entra ID checks expand the coverage of CIS Microsoft 365 Foundations Benchmark v7.0.0:
- **Password protection:** custom banned password list, on-premises enforcement, and lockout threshold and duration.
- **Default user permissions:** security group and Microsoft 365 group creation restricted, and guest invitations limited to allowed domains.
- **Conditional Access:** high and medium sign-in risk blocked, authentication transfer blocked, untrusted locations blocked, trusted named locations defined, sign-in frequency enforced, and token protection enforced.
- **Sessions and authentication methods:** idle session timeout configured, email one-time passcodes disabled, and Microsoft Authenticator context shown.
- **PIM and access reviews:** approval required to activate the Global Administrator and Privileged Role Administrator roles, and access reviews configured for guest users and privileged roles.
Explore all Microsoft 365 checks at [Prowler Hub](https://hub.prowler.com/check?provider=m365).
#### AWS
Two new checks detect hardcoded secrets:
- `batch_job_definition_no_secrets` scans Batch job definition environment variables and command parameters. Thanks to @praneetrajv!
- `awslambda_layer_no_secrets_in_content` scans Lambda layer package content. Thanks to @ganiganesh25!
Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
### 🙌 External Contributors
Thank you to our community contributors for this release!
- @praneetrajv: AWS `batch_job_definition_no_secrets` check ([#12117](https://github.com/prowler-cloud/prowler/pull/12117))
- @ganiganesh25: AWS `awslambda_layer_no_secrets_in_content` check ([#12233](https://github.com/prowler-cloud/prowler/pull/12233))
- @andoniaf: GitHub `organization_repository_creation_limited` now reports low severity when repository creation is limited to private or internal visibility ([#12164](https://github.com/prowler-cloud/prowler/pull/12164))
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.39.0) for the complete list of changes.
</Update>
<Update label="v5.38.0" description="August 6, 2026">
### 📌 Compliance Watchlist
@@ -165,7 +498,7 @@ rss: true
All checks are fully passive, using AWS APIs and CloudTrail with no instance access or SSM agent required, and are mapped across 23 compliance frameworks, including NIST 800-53 Rev 5, PCI-DSS v4.0, ISO 27001:2022, SOC 2, HIPAA, and MITRE ATT&CK.
Read more about it this [blog post](https://prowler.com/blog/your-llm-runs-in-a-nitro-enclave-who-is-checking-the-enclave).
Read more about it in this [blog post](https://prowler.com/blog/your-llm-runs-in-a-nitro-enclave-who-is-checking-the-enclave).
Try them out now at [cloud.prowler.com](https://cloud.prowler.com/sign-up)!
+2 -2
View File
@@ -57,7 +57,7 @@ The AWS provider implementation follows the general [Provider structure](/develo
The generic service pattern is described in [service page](/developer-guide/services#service-structure-and-initialisation). You can find all the right now implemented services in the following locations:
- Directly in the code, in location [`prowler/providers/aws/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/aws/services)
- In the [Prowler Hub](https://hub.prowler.com/). For a more human-readable view.
- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used across all AWS services.
@@ -131,7 +131,7 @@ def _get_email_identities(self, identity):
The AWS checks pattern is described in [checks page](/developer-guide/checks). You can find all the right now implemented checks:
- Directly in the code, within each service folder, each check has its own folder named after the name of the check. (e.g. [`prowler/providers/aws/services/s3/s3_bucket_acl_prohibited/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/aws/services/s3/s3_bucket_acl_prohibited))
- In the [Prowler Hub](https://hub.prowler.com/). For a more human-readable view.
- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
The best reference to understand how to implement a new check is following the [check creation documentation](/developer-guide/checks#creating-a-check) and taking other similar checks as reference.
+34 -3
View File
@@ -129,12 +129,42 @@ Each check **must** populate the `report.status` and `report.status_extended` fi
- Status field: `report.status`
- `PASS` – Assigned when the check confirms compliance with the configured value.
- `FAIL` – Assigned when the check detects non-compliance with the configured value.
- `MANUAL` – This status must not be used unless manual verification is necessary to determine whether the status (`report.status`) passes (`PASS`) or fails (`FAIL`).
- `MANUAL` – This status must not be used unless manual verification is necessary to determine whether the status (`report.status`) passes (`PASS`) or fails (`FAIL`). This includes the case where Prowler could not retrieve the data needed to evaluate the resource (see below).
- Status extended field: `report.status_extended`
- It **must** end with a period (`.`).
- It **must** include the audited service, the resource, and a concise explanation of the check result, for instance: `EC2 AMI ami-0123456789 is not public.`.
### Permission and Data-Availability Errors Are Not Findings
A `FAIL` must only be emitted when an insecure condition has actually been detected. A check **must never** report `FAIL` because the underlying API call failed: missing permissions or scopes on the scanning identity, an API that is not enabled, a feature that is not licensed, or data that could not be retrieved are scan-configuration problems, not security issues. Reporting them as `FAIL` surfaces a misleading (and often high-severity) finding to the user and skews compliance scores.
When the service layer cannot obtain the data a check depends on, the check must:
1. Emit a single `MANUAL` finding scoped to the widest affected resource (the tenant, account, project or subscription), not one finding per resource. For example, if user registration details cannot be read, emit one tenant-level `MANUAL` instead of one per user.
2. Explain in `status_extended` that the check could not be evaluated and what to fix, naming the permission, scope, API or license required, for instance: `Cannot evaluate credential exposure for privileged users: unable to query Microsoft Defender XDR Advanced Hunting. Verify that the ThreatHunting.Read.All permission is granted to the scanning application.`
3. Leave the check's severity untouched. Do not override `report.check_metadata.Severity` to hide the problem.
The service layer must make the distinction possible: log the error and expose it to checks in a way that cannot be confused with a legitimate empty result. Common patterns already used in Prowler are:
- Defaulting the attribute to `None` (data could not be read) instead of `[]`/`{}` (data was read and is empty), e.g. the `metric_filters is not None` guard in `prowler/providers/aws/services/cloudwatch/lib/metric_filters.py`.
- Keeping an availability flag raised on any denied listing, e.g. `logs_client.metric_filters_unavailable` consumed by the AWS CloudWatch metric filter checks.
- Keeping an error flag or message next to the data, e.g. `entra_client.user_registration_details_error` in M365 or `*_scan_errors` in AWS Bedrock.
- Keeping a set of resources whose lookup failed, e.g. `accessapproval_client.settings_lookup_failed` in GCP.
Make sure the error branch only captures real access errors. A `404`/not-found response frequently means the feature is simply not configured, which **is** a legitimate `FAIL`; a `403` or an unexpected exception is not. An "API not enabled" error is usually a scan-configuration problem too — **except** when the API's activation is itself the control being audited (e.g. GCP Access Approval: with `accessapproval.googleapis.com` disabled the feature provably cannot be enabled, so a definitive API-disabled state is a legitimate `FAIL`, while an undetermined state stays `MANUAL`).
```python
if <service>_client.<data> is None:
report = CheckReport<Provider>(metadata=self.metadata(), resource={})
report.resource_name = "<Tenant/Account-level resource>"
report.resource_id = "<stable-id>"
report.status = "MANUAL"
report.status_extended = "Cannot evaluate <requirement>: <data> could not be retrieved. Verify that <permission/API/license> is granted to the scanning identity."
findings.append(report)
return findings
```
### Prowler's Check Severity Levels
The severity of each check is defined in the metadata file using the `Severity` field. Severity values are always lowercase and must be one of the predefined categories below.
@@ -224,9 +254,9 @@ Each check **must** populate the report with a unique identifier for the audited
- `resource_name`: Description of the configuration (e.g., "SharePoint Settings")
- GitHub
- Resource ID — `report.resource_id`.
- The ID of the Github resource. This is a system-generated integer that uniquely identifies the resource within the Github platform.
- The ID of the GitHub resource. This is a system-generated integer that uniquely identifies the resource within the GitHub platform.
- Resource Name — `report.resource_name`.
- The name of the Github resource. In the case of a repository, this is just the repository name. For full repository names use the resource `full_name`.
- The name of the GitHub resource. In the case of a repository, this is just the repository name. For full repository names use the resource `full_name`.
### Configurable Checks in Prowler
@@ -437,6 +467,7 @@ The metadata structure is enforced in code using a Pydantic model. For reference
- Use clear, actionable, and user-friendly language in `status_extended` to explain the result. Always provide information to identify the resource.
- Use helper functions/utilities for repeated logic to avoid code duplication. Save them in the `lib` folder of the service.
- Handle exceptions gracefully: catch errors per resource, log them, and continue processing other resources.
- Never report `FAIL` because data could not be retrieved (missing permissions, API not enabled, feature not licensed). Emit a single `MANUAL` finding explaining what is required instead; see [Permission and Data-Availability Errors Are Not Findings](#permission-and-data-availability-errors-are-not-findings).
- Document the check with a class and function level docstring explaining what it does, what it checks, and any caveats or provider-specific behaviors.
- Use type hints for the `execute()` method (e.g., `-> list[CheckReport<Provider>]`) for clarity and static analysis.
- Ensure checks are efficient; avoid excessive nested loops. If the complexity is high, consider refactoring the check.
+1 -1
View File
@@ -10,7 +10,7 @@ Visual Studio Code (also referred to as VSCode) provides an integrated debugger
### Debugging Configuration Example
The following file is an example of a [debugging configuration](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations) file for [Virtual Studio Code](https://code.visualstudio.com/).
The following file is an example of a [debugging configuration](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations) file for [Visual Studio Code](https://code.visualstudio.com/).
This file must be placed inside the *.vscode* directory and named *launch.json*:
+3 -3
View File
@@ -50,7 +50,7 @@ When adding or maintaining E2E tests for Prowler Local Server, follow these guid
```
5. **Tag and document scenarios**
- Follow the existing naming convention for suites and test cases (for example, `SCANS-E2E-001`, `PROVIDER-E2E-003`) and use tags such as `@e2e`, `@serial` and feature tags (for example, `@providers`, `@scans`,`@aws`) to filter and organize tests.
- Follow the existing naming convention for suites and test cases (for example, `SCANS-E2E-001`, `PROVIDER-E2E-003`) and use tags such as `@e2e`, `@serial` and feature tags (for example, `@providers`, `@scans`, `@aws`) to filter and organize tests.
**Example:**
```typescript
@@ -71,7 +71,7 @@ When adding or maintaining E2E tests for Prowler Local Server, follow these guid
}
);
```
- Document each one in the Markdown files under `ui/tests`, including **Priority**, **Tags**, **Description**, **Preconditions**, **Flow steps**, **Expected results**,**Key verification points** and **Notes**.
- Document each one in the Markdown files under `ui/tests`, including **Priority**, **Tags**, **Description**, **Preconditions**, **Flow steps**, **Expected results**, **Key verification points** and **Notes**.
**Example**
```Markdown
@@ -256,7 +256,7 @@ To execute E2E tests for Prowler Local Server:
pnpm run test:e2e
```
This command runs Playwright with the configured projects
This command runs Playwright with the configured projects.
2. **Run E2E tests with the Playwright UI runner**
+57 -13
View File
@@ -120,14 +120,14 @@ class NewFeatureTools(BaseTool):
Returns complete feature details including configuration and metadata.
"""
try:
response = await self.api_client.get(f"/api/v1/features/{feature_id}")
return DetailedFeature.from_api_response(response["data"]).model_dump()
except Exception as e:
self.logger.error(f"Failed to get feature {feature_id}: {e}")
return {"error": str(e), "status": "failed"}
response = await self.api_client.get(f"/api/v1/features/{feature_id}")
return DetailedFeature.from_api_response(response["data"]).model_dump()
```
There is no `try`/`except` here on purpose. A failed request raises, and
[Error Handling](#error-handling) explains what turns that raise into a message
the agent can act on.
### Step 2: Create the Models
Create corresponding models in `prowler_app/models/`:
@@ -369,18 +369,62 @@ async def search_items(self, status: str = Field(...)) -> dict:
### Error Handling
Return structured error responses instead of raising exceptions:
**Raise, never return.** A returned `{"error": ...}` dict is reported to the
client as `isError: false` -- a *successful* tool call whose payload happens to
mention a failure. Clients and models read that as success. A raised exception
becomes a spec-correct tool execution error instead.
The common case therefore needs no handler at all:
```python
async def get_item(self, item_id: str) -> dict:
try:
response = await self.api_client.get(f"/api/v1/items/{item_id}")
return DetailedItem.from_api_response(response["data"]).model_dump()
except Exception as e:
self.logger.error(f"Failed to get item {item_id}: {e}")
return {"error": str(e), "status": "failed"}
response = await self.api_client.get(f"/api/v1/items/{item_id}")
return DetailedItem.from_api_response(response["data"]).model_dump()
```
`prowler_mcp_server/lib/errors.py` classifies the failures every tool shares --
a rejected credential, a missing permission, a rate limit, an outage, an
unreachable API, a bad argument -- and gives each one a message that says what
went wrong and what to do about it. Anything it does not recognise is masked,
because `mask_error_details=True` is set on every sub-server and upstream
response bodies must never be replayed into a model's context.
Three ways to raise, in the order to reach for them:
```python
from fastmcp.exceptions import ToolError
from prowler_mcp_server.lib.errors import InvalidArgument
# 1. An argument this server rejected before any request went out. The message
# is repeated to the agent verbatim, so write it for one to read.
if not 1 <= page_size <= 1000:
raise InvalidArgument("page_size must be between 1 and 1000.")
# 2. A request the API answered or never answered: let it propagate untouched.
# `ProwlerAPIError` and `ProwlerAPIUnreachable` are what the classifier keys
# on, and the second one is what stops a retry from duplicating a write.
response = await self.api_client.get(f"/api/v1/items/{item_id}")
data = response["data"]
# 3. A sentence the classifier cannot know -- a resource name, a precondition,
# the next tool to call. NOTE the absent `from` clause: it is what marks the
# message as already final. With `from e` the classifier would replace it.
if not data:
raise ToolError(
f"No item with the ID {item_id!r} exists. Use prowler_list_items to "
"find a valid one."
)
```
The one thing that still *returns* rather than raises is a write whose outcome is
genuinely unknown. `prowler_send_findings_to_jira` is the worked example: work
items are created one at a time and Prowler cannot delete them, so a dispatch
that stopped halfway answers with a result object carrying
`safe_to_retry: false`. "This may have been applied" is a fact about the world,
not an error, and squashing it into one loses the only thing that stops a retry
from duplicating the write.
### Parameter Descriptions
Use Pydantic `Field()` with clear descriptions. This also helps LLMs understand
+10 -10
View File
@@ -25,7 +25,7 @@ For providers supported by Prowler, refer to [Prowler Hub](https://hub.prowler.c
Prowler supports several types of providers, each with its own implementation pattern and use case. Understanding these differences is key to designing your provider correctly.
### Classifying your Provider
### Classifying Your Provider
Before implementing a new provider, you need to determine which type it belongs to. This classification will guide your implementation approach and help you choose the right patterns and libraries.
@@ -107,7 +107,7 @@ Once you have decided the provider you want or need to add to Prowler, the next
- **SDK Providers**: Low complexity. You have mature examples like AWS, Azure, GCP, Kubernetes, etc. that you can leverage to implement your provider.
- **API Providers**: Medium complexity. You need to implement the authentication and session management, and the API calls to the provider. You now have NHN and MongoDB Atlas as example to follow.
- **Tool/Wrapper Providers**: High complexity. You need to implement the argument/output mapping to the provider and handle problems that the tool/wrapper may have. You now have IAC and the PowerShell wrapper as example to follow.
- **Hybrid Providers**: High complexity. You need to "customize" your provider, mixing the other types of providers in order to achieve the desired result. You have M365 (msgraph SDK + PowerShell wrapper) and Github (PyGithub SDK + graphql API requests) as examples.
- **Hybrid Providers**: High complexity. You need to "customize" your provider, mixing the other types of providers to achieve the desired result. You have M365 (msgraph SDK + PowerShell wrapper) and GitHub (PyGithub SDK + graphql API requests) as examples.
### Determining Regional vs Non-Regional Architecture
@@ -814,7 +814,7 @@ class YourProviderMutelist(Mutelist):
Region management is essential for cloud providers that operate across multiple geographic locations. This component handles region validation and provides region-specific functionality.
<Note>
Regions are optional, only if the provider has regions, for example Github does not have regions, but AWS does.
Regions are optional, only if the provider has regions, for example GitHub does not have regions, but AWS does.
</Note>
**File:** `prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py`
@@ -1090,7 +1090,7 @@ Main registration makes your provider discoverable by Prowler's core system. It'
cis.batch_write_data_to_file()
```
#### Step 11: Register in the list of providers
#### Step 11: Register in the List of Providers
**Explanation:**
This is needed to be able to use the provider in the generic checks. The provider must be registered in the `init_global_provider` method to handle CLI arguments and initialization.
@@ -1773,7 +1773,7 @@ The implementation of the mutelist is the same as the [SDK providers](#step-5-im
Region management is essential for cloud providers that operate across multiple geographic locations. This component handles region validation and provides region-specific functionality.
<Note>
Regions are optional, only if the provider has regions, for example Github does not have regions, but AWS does.
Regions are optional, only if the provider has regions, for example GitHub does not have regions, but AWS does.
</Note>
**File:** `prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py`
@@ -1966,7 +1966,7 @@ Main registration makes your provider discoverable by Prowler's core system. It'
This step is the same as the [SDK providers](#step-10-register-in-main).
#### Step 11: Register in the list of providers
#### Step 11: Register in the List of Providers
**Explanation:**
This is needed to be able to use the provider in the generic checks. The provider must be registered in the `init_global_provider` method to handle CLI arguments and initialization.
@@ -2648,7 +2648,7 @@ Main registration makes your provider discoverable by Prowler's core system. It'
This step is the same as the [SDK providers](#step-10-register-in-main).
#### Step 7: Register in the list of providers
#### Step 7: Register in the List of Providers
**Explanation:**
This is needed to be able to use the provider in the generic checks. The provider must be registered in the `init_global_provider` method to handle CLI arguments and initialization.
@@ -2808,7 +2808,7 @@ def validate_your_provider_uid(value):
**Provider Model:**
The `Provider` model already exists and supports all provider types. Ensure your provider type is included in the choices.
### 2.2. Add the provider to the Provider Choices
### 2.2. Add the Provider to the Provider Choices
Update the `return_prowler_provider` function to include your provider. This function is crucial for the API to instantiate the correct provider class.
@@ -3209,7 +3209,7 @@ class YourProviderAPITestCase(APITestCase):
self.assertEqual(response.status_code, 201)
```
#### 2.6.1. Add your mocked provider to the tests
#### 2.6.1. Add Your Mocked Provider to the Tests
If needed, add a named provider fixture or extend the provider factory defaults so tests can request only the provider they need.
@@ -3272,7 +3272,7 @@ Your provider will be available through these endpoints:
- `DELETE /api/v1/providers/{id}/` - Delete provider
- `POST /api/v1/providers/secrets/` - Add provider credentials
### 2.9. Update the provider if needed
### 2.9. Update the Provider If Needed
Depending on your provider's authentication requirements, you may need to add new authentication methods that are compatible with the API. This involves updating the provider class to support additional credential types beyond the basic ones.
@@ -18,7 +18,7 @@ A compliance framework must represent the **complete state** of the source catal
Requirement coverage feeds the compliance percentage calculations and the metadata surfaces (dashboards, widgets, exports). Missing requirements skew those metrics and break the report as a faithful snapshot of the framework.
</Warning>
### Two supported schemas
### Two Supported Schemas
| Schema | When to use | File location | Discovered as |
| --- | --- | --- | --- |
@@ -45,7 +45,7 @@ Before adding a new framework, complete the following checks:
## Universal Compliance Framework
### Where the file lives
### Where the File Lives
Place the file at the top level of the compliance directory:
@@ -57,7 +57,7 @@ Examples in the repository: `prowler/compliance/csa_ccm_4.0.json`, `prowler/comp
The file is auto-discovered — there is **no** need to register it in any `__init__.py`, modify `prowler/lib/outputs/`, or update any other Python module. The framework key Prowler CLI accepts via `--compliance` is the basename of the JSON file without `.json` (`dora_2022_2554.json` → `dora_2022_2554`).
### Top-level structure
### Top-Level Structure
```json
{
@@ -198,7 +198,7 @@ Per requirement:
For MITRE-style frameworks, additional optional fields are available on the requirement: `tactics`, `sub_techniques`, `platforms`, `technique_url` (these are populated automatically when adapting a legacy MITRE JSON to the universal model).
### Multi-provider frameworks
### Multi-Provider Frameworks
A single universal file can cover any number of providers. The framework appears under each provider's `--list-compliance` output as long as **at least one** requirement has that provider key in its `checks` dict.
@@ -226,7 +226,7 @@ The legacy schema spans **four layers** — a complete contribution must touch e
The universal schema collapses Layers 3 and 4 into declarative configuration inside the JSON — that is the main reason it is preferred for new contributions.
### Directory structure and file naming
### Directory Structure and File Naming
Compliance frameworks live at:
@@ -259,7 +259,7 @@ prowler/lib/outputs/compliance/<framework>/
└── __init__.py
```
### JSON schema reference
### JSON Schema Reference
Every legacy compliance file is a JSON document with the following top-level keys. `Framework`, `Name` and `Provider` are validated non-empty by the root validator `framework_and_provider_must_not_be_empty` (`compliance_models.py`).
@@ -362,7 +362,7 @@ For the remaining attribute classes (`AWS_Well_Architected_Requirement_Attribute
The `Attributes` field is a Pydantic `Union`. The generic attribute model **must** remain the last element of that Union — otherwise Pydantic v1 silently coerces every framework into the generic shape and your specialized fields are dropped. Adding a brand-new attribute shape requires inserting the Pydantic class **before** `Generic_Compliance_Requirement_Attribute`.
</Note>
#### Minimal working example
#### Minimal Working Example
The following snippet is a complete, valid framework file named `my_framework_1.0_aws.json`, saved at `prowler/compliance/aws/my_framework_1.0_aws.json`. It uses the generic attribute shape for simplicity.
@@ -408,7 +408,7 @@ The following snippet is a complete, valid framework file named `my_framework_1.
}
```
### Mapping checks to requirements
### Mapping Checks to Requirements
Each requirement links to the Prowler checks that, together, produce a PASS or FAIL verdict for that control.
@@ -425,7 +425,7 @@ To discover available checks:
uv run python prowler-cli.py <provider> --list-checks
```
### Supporting multiple providers (legacy)
### Supporting Multiple Providers (Legacy)
The legacy schema binds each file to a single provider. To cover several providers with the same framework, ship one JSON file per provider:
@@ -439,7 +439,7 @@ Keep the `Framework` and `Version` values identical across the files so the disp
For a brand-new framework that spans several providers, **prefer the universal schema** — it covers every provider from a single file. If you must use the legacy schema, add one transformer per provider in `prowler/lib/outputs/compliance/<framework>/` and extend the summary-table dispatcher accordingly. See [Output Formatter](#output-formatter).
### Output formatter
### Output Formatter
Legacy frameworks render in two forms: a detailed CSV report written to disk, and a summary table printed in the CLI. Both are produced by the output formatter package for the framework. Universal frameworks do **not** need a Python output formatter — the `outputs` config inside the JSON drives rendering — so this section applies only to the legacy schema.
@@ -453,19 +453,19 @@ prowler/lib/outputs/compliance/my_framework/
└── models.py # CSV row Pydantic model
```
#### Step 1 — Define the CSV row model
#### Step 1 — Define the CSV Row Model
In `models.py`, declare a Pydantic v1 model with one field per CSV column. Use existing models such as `AWSCISModel` in `prowler/lib/outputs/compliance/cis/models.py` as the reference. Fields typically include `Provider`, `Description`, `AccountId`, `Region`, `AssessmentDate`, `Requirements_Id`, `Requirements_Description`, one `Requirements_Attributes_*` field per attribute key, plus the finding fields `Status`, `StatusExtended`, `ResourceId`, `ResourceName`, `CheckId`, `Muted`, `Framework`, `Name`.
#### Step 2 — Implement the transformer
#### Step 2 — Implement the Transformer
In `my_framework_aws.py`, subclass `ComplianceOutput` from `prowler.lib.outputs.compliance.compliance_output` and implement `transform(findings, compliance, compliance_name)`. Iterate over `findings`, match each finding to the requirements it satisfies through `finding.compliance.get(compliance_name, [])`, and append one row per attribute to `self._data`.
#### Step 3 — Add the summary-table dispatcher
#### Step 3 — Add the Summary-Table Dispatcher
In `my_framework.py`, implement `get_my_framework_table(findings, bulk_checks_metadata, compliance_framework, output_filename, output_directory, compliance_overview)` following the pattern in `prowler/lib/outputs/compliance/cis/cis.py`.
#### Step 4 — Register the framework in the dispatchers
#### Step 4 — Register the Framework in the Dispatchers
- Add the dispatcher call in `prowler/lib/outputs/compliance/compliance.py`, inside `display_compliance_table`, with a branch such as `elif "my_framework" in compliance_framework:`.
- Register the CSV model and transformer in `prowler/lib/outputs/compliance/compliance_output.py` so the CSV file is emitted during the scan.
@@ -474,7 +474,7 @@ In `my_framework.py`, implement `get_my_framework_table(findings, bulk_checks_me
For NIST-style catalogs that use `Generic_Compliance_Requirement_Attribute`, no custom formatter is needed. The generic formatter in `prowler/lib/outputs/compliance/generic/` handles them automatically, provided the JSON validates against the generic attribute schema.
</Note>
### Legacy-to-universal adapter
### Legacy-to-Universal Adapter
At load time, every legacy file is transparently adapted to a `ComplianceFramework` via `adapt_legacy_to_universal()` (`compliance_models.py`), which: (a) flattens the first element of `Attributes` into a flat `attributes` dict, (b) wraps `Checks` as `{provider_lower: [...]}`, (c) infers `attributes_metadata` from the matched Pydantic class via `_infer_attribute_metadata()`. The rest of Prowler (CSV/OCSF/PDF output, CLI table) then treats both formats identically.
@@ -497,7 +497,7 @@ Configuration guardrails close that gap. A requirement declares the configuratio
Guardrails are an **optional** safety net for configurable checks. A requirement that maps only to non-configurable checks does not need them. When the field is absent, behavior is unchanged.
</Note>
### Where guardrails are declared
### Where Guardrails Are Declared
The field is attached to each requirement and exists in both schemas:
@@ -506,7 +506,7 @@ The field is attached to each requirement and exists in both schemas:
When a legacy file is adapted to the universal model, `adapt_legacy_to_universal()` copies `ConfigRequirements` into `config_requirements` (`compliance_models.py`), so downstream code only ever reads one shape.
### Constraint schema
### Constraint Schema
Each entry in the list is a single constraint with the following fields:
@@ -533,7 +533,7 @@ Each entry in the list is a single constraint with the following fields:
`subset` / `superset` require both the applied value and `Value` to be lists; any other type is treated as not satisfied. For `eq` against a boolean, declare `Value` as a JSON boolean (`false`, not `0`) — the model keeps booleans distinct from integers.
</Note>
### How guardrails are evaluated
### How Guardrails Are Evaluated
All evaluation lives in one shared module, `prowler/lib/check/compliance_config_eval.py`, consumed by every compliance output (CSV, OCSF, and the CLI tables) and reused by the Prowler API backend so the rule is defined exactly once.
@@ -547,7 +547,7 @@ All evaluation lives in one shared module, `prowler/lib/check/compliance_config_
Guardrails only ever make a result **stricter** (they can turn PASS into FAIL); they never relax a real FAIL into PASS. A requirement with no constraints, or whose keys all use defaults, is reported exactly as before.
</Warning>
### Example: legacy framework
### Example: Legacy Framework
From `prowler/compliance/aws/cis_6.0_aws.json`, requirement 2.11 declares two guardrails — one per configurable check it maps to:
@@ -590,7 +590,7 @@ A boolean guardrail from the same file: requirement 2.5 (IAM Access Analyzer) on
]
```
### Example: universal framework
### Example: Universal Framework
The universal schema uses the lowercase `config_requirements` key with the identical object shape:
@@ -616,7 +616,7 @@ The universal schema uses the lowercase `config_requirements` key with the ident
Each constraint declares the `Provider` it targets so the guardrail is only evaluated on scans of that provider — essential for universal frameworks like CSA CCM and DORA, where one requirement maps checks across `aws`, `azure`, `gcp` and more. Because the operator is `subset`, adding `"TLS 1.0"` to `recommended_minimal_tls_versions` widens the allowlist beyond `["TLS 1.2", "TLS 1.3"]` and the requirement is forced to FAIL.
### What the user sees
### What the User Sees
With a loosened config, the affected requirement's findings report:
@@ -630,7 +630,7 @@ StatusExtended: Configuration not valid for this requirement. The check
The same `Configuration not valid for this requirement.` message appears identically across the CSV, OCSF, and console-table outputs.
### Authoring guidelines
### Authoring Guidelines
- Declare a guardrail only for keys whose value actually changes whether the requirement is met. Most configurable checks do not need one.
- Set `Value` to the **strictest** configuration the control tolerates — the same number the control text cites (CIS 45 days, NIST ≤90, and so on).
@@ -639,7 +639,7 @@ The same `Configuration not valid for this requirement.` message appears identic
- Pick the operator from the value's role: a max threshold is `lte`, a min threshold is `gte`, a toggle is `eq`, an allowlist is `subset`, a denylist is `superset`.
- An unrecognized operator does **not** block the requirement — a malformed constraint is treated as satisfied rather than failing the whole framework. Validate your JSON with the tests below.
### Testing guardrails
### Testing Guardrails
The shared evaluator and the per-output integration are covered by:
@@ -670,7 +670,7 @@ Prowler matches frameworks by concatenating `Framework` and `Version`. A missing
Before opening a PR, validate the JSON loads cleanly against the model and that every referenced check actually exists.
### 1. Schema validation
### 1. Schema Validation
For **universal** frameworks, load the file and inspect what was parsed. The framework key inside `bulk` is the **basename of the JSON file** (without `.json`); for `prowler/compliance/dora_2022_2554.json` that key is `dora_2022_2554`, for `prowler/compliance/aws/cis_5.0_aws.json` it is `cis_5.0_aws`.
@@ -688,7 +688,7 @@ bulk = get_bulk_compliance_frameworks_universal("aws")
assert "<your_framework_filename_without_json>" in bulk
```
### 2. Check existence cross-check
### 2. Check Existence Cross-Check
There is **no automatic check-existence validation** at load time. Cross-check that every check name in your framework maps to a real check directory:
@@ -708,7 +708,7 @@ missing = referenced - real
assert not missing, f"checks referenced in framework but not found in repo: {sorted(missing)}"
```
### 3. CLI smoke test
### 3. CLI Smoke Test
```bash
uv run python prowler-cli.py <provider> --list-compliance
@@ -728,7 +728,7 @@ Verify that:
- The CLI summary table lists every section / pillar of the framework.
- Findings roll up under the expected requirements.
### 4. Inspect the CSV output
### 4. Inspect the CSV Output
Open the generated CSV and confirm:
+9 -9
View File
@@ -12,7 +12,7 @@ This guide explains how to add a **Server-Sent Events (SSE)** endpoint to the Pr
The platform ships the SSE **infrastructure** (`api.sse`) and wiring. No feature endpoint streams over SSE out of the box — this guide shows how to build one on top of the shared base.
</Info>
## When to use SSE
## When to Use SSE
| Need | Use |
|------|-----|
@@ -22,7 +22,7 @@ The platform ships the SSE **infrastructure** (`api.sse`) and wiring. No feature
SSE is the right tool when the **client only consumes**: scan progress, long-running job checkpoints, streamed LLM tokens, cross-client resource-sync notifications. It rides on plain HTTP, reconnects automatically in the browser via the native [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API, and needs no extra protocol.
## How it works
## How It Works
SSE is wired through [`django-eventstream`](https://github.com/fanout/django_eventstream) and a small platform layer in `api/src/backend/api/sse/`:
@@ -34,11 +34,11 @@ SSE is wired through [`django-eventstream`](https://github.com/fanout/django_eve
| `make_channel_name` / `tenant_id_from_channel` | `api/sse/utils.py` | Single source of truth for the channel-name format, so publishers and the channel manager agree byte-for-byte. |
| Settings | `config/settings/eventstream.py` | Valkey Pub/Sub backend (dedicated DB), channel manager, allowed headers. |
### Transport: the server runs on ASGI
### Transport: The Server Runs on ASGI
SSE connections are long-lived. Holding one open per synchronous worker would exhaust the worker pool, so the API runs under Gunicorn's native **`asgi` worker** (`config.asgi:application`). Streams are parked on the event loop while ordinary CRUD endpoints keep their synchronous execution (Django runs sync views in a thread-sensitive executor under ASGI). This is configured in `config/guniconf.py` and used by both the dev and production entrypoints — no separate server process is needed.
### The data flow
### The Data Flow
```
publisher (Celery task / view) subscriber (browser, CLI)
@@ -53,7 +53,7 @@ publisher (Celery task / view) subscriber (browser, CLI)
A publisher anywhere in the system (most often a Celery task) calls `send_event(channel, event_type, payload)`. `django-eventstream` fans it out over Valkey Pub/Sub to every connection subscribed to that channel.
## Adding an SSE endpoint to your feature
## Adding an SSE Endpoint to Your Feature
The example below streams progress for a long-running **scan**. Adapt the resource, prefix, and event names to your feature.
@@ -161,7 +161,7 @@ publish_end(channel, scan_id=str(scan.id))
</Steps>
## Event naming convention
## Event Naming Convention
Every event uses an event type of the form **`<resource>.<verb>`** (lowercased, dot-separated). The verb comes from this platform-wide vocabulary — if you need a verb that is not listed, document the addition in this guide so the catalog stays discoverable.
@@ -197,7 +197,7 @@ curl -N -H "Authorization: Bearer $JWT" \
https://<host>/api/v1/scans/$SCAN_ID/event-stream
```
## Tenant isolation & security model
## Tenant Isolation & Security Model
Authorization is enforced at two layers:
@@ -206,7 +206,7 @@ Authorization is enforced at two layers:
Because the tenant id lives inside the channel name, this gate works for any feature without the platform knowing anything about it.
## Reconnect & state recovery
## Reconnect & State Recovery
The platform deliberately ships **without server-side replay** (`is_channel_reliable` returns `False`). When a client reconnects, it does **not** receive missed events. Instead:
@@ -215,7 +215,7 @@ The platform deliberately ships **without server-side replay** (`is_channel_reli
Design your event payloads accordingly: deltas are ephemeral and concatenated in-flight; the durable truth always lives behind a REST resource.
## Local development
## Local Development
- The dev and production entrypoints both launch Gunicorn with the `asgi` worker (`config.asgi:application`). In dev, `DJANGO_DEBUG=True` enables hot reload; `preload_app` is automatically disabled under debug so edited code is picked up.
- SSE uses a **dedicated Valkey database** (`EVENTSTREAM_VALKEY_DB`, default `2`) kept separate from the Celery broker so a noisy broker cannot crowd out streaming traffic. It reuses the same `VALKEY_*` connection settings as the rest of the platform.
+1 -1
View File
@@ -537,7 +537,7 @@ This architecture allows Prowler to efficiently scan AWS accounts with resources
## Best Practices
- When available in the provider, use threading or parallelization utilities for all methods that can be parallelized by to maximize performance and reduce scan time.
- When available in the provider, use threading or parallelization utilities for all methods that can be parallelized to maximize performance and reduce scan time.
- Define a Pydantic `BaseModel` for every resource you manage, and use these models for all resource data handling.
- Log every major step (start, success, error) in resource discovery and attribute collection for traceability and debugging; include as much context as possible.
- Catch and log all exceptions, providing detailed context (region, subscription, resource, error type, line number) to aid troubleshooting.
+5 -5
View File
@@ -154,7 +154,7 @@ Failing to update this table when adding cross-service dependencies may result i
For AWS provider, different testing approaches apply based on API coverage based on several criteria.
<Note>
Prowler leverages and contributes to the[Moto](https://github.com/getmoto/moto) library for mocking AWS infrastructure in tests.
Prowler leverages and contributes to the [Moto](https://github.com/getmoto/moto) library for mocking AWS infrastructure in tests.
</Note>
- AWS API Calls Covered by [Moto](https://github.com/getmoto/moto):
@@ -408,7 +408,7 @@ In all above scenarios, check execution must occur within the context of mocked
When a service requires API calls that are partially covered by the Moto decorator, additional mocking is necessary. In such cases, custom mocked API calls must be implemented alongside Moto to ensure full coverage.
To achieve this, mock the `botocore.client.BaseClient._make_api_call` function—the method responsible for making actual API requests to AWS—using `mock.patch <https://docs.python.org/3/library/unittest.mock.html#patch>`:
To achieve this, mock the `botocore.client.BaseClient._make_api_call` function—the method responsible for making actual API requests to AWS—using [`mock.patch`](https://docs.python.org/3/library/unittest.mock.html#patch):
```python
@@ -475,7 +475,7 @@ However, if additional `moto` decorators are applied alongside the patch, Moto w
</Note>
<Note>
The source of the above implementation can be found here:[Patch Other Services with Moto](https://docs.getmoto.org/en/latest/docs/services/patching\_other\_services.html)
The source of the above implementation can be found here: [Patch Other Services with Moto](https://docs.getmoto.org/en/latest/docs/services/patching_other_services.html)
</Note>
#### Mocking Several Services
@@ -525,7 +525,7 @@ with mock.patch(
):
```
As demonstrated in the code above, mocking both the AWS audit information and all utilized services is mandatory for proper test execution.
As demonstrated in the code above, mocking both the AWS audit information and all used services is mandatory for proper test execution.
#### Patching vs. Importing
@@ -603,7 +603,7 @@ with mock.patch(
will cause that the service is initialized only once—at the moment of mocking out `set_mocked_aws_provider([<region>])` using `mock.patch`.
Later, when Python attempts to import the client at the check level, the execution continues using`from prowler.providers.<provider>.services.<service>.<service>_client`. As a result of it being already mocked out, the execution will continue using `service_client` without getting into `<service>_client.py`.
Later, when Python attempts to import the client at the check level, the execution continues using `from prowler.providers.<provider>.services.<service>.<service>_client`. As a result of it being already mocked out, the execution will continue using `service_client` without getting into `<service>_client.py`.
### Testing AWS Services
+12 -1
View File
@@ -182,7 +182,8 @@
"pages": [
"user-guide/tutorials/prowler-app-s3-integration",
"user-guide/tutorials/prowler-app-security-hub-integration",
"user-guide/tutorials/prowler-app-jira-integration"
"user-guide/tutorials/prowler-app-jira-integration",
"user-guide/tutorials/prowler-app-slack-integration"
]
},
{
@@ -301,6 +302,15 @@
{
"group": "Providers",
"pages": [
{
"group": "Organizations",
"pages": [
"user-guide/organizations",
"user-guide/providers/aws/organizations",
"user-guide/providers/gcp/organization",
"user-guide/providers/azure/management-groups"
]
},
{
"group": "Alibaba Cloud",
"pages": [
@@ -330,6 +340,7 @@
"user-guide/providers/azure/getting-started-azure",
"user-guide/providers/azure/authentication",
"user-guide/providers/azure/use-non-default-cloud",
"user-guide/providers/azure/management-groups",
"user-guide/providers/azure/subscriptions",
"user-guide/providers/azure/resource-groups",
"user-guide/providers/azure/create-prowler-service-principal"
@@ -4,7 +4,7 @@ title: 'Basic Usage'
## Running Prowler
Running Prowler requires specifying the provider (e.g `aws`, `gcp`, `azure`, `kubernetes`, `m365`, `github`, `iac` or `mongodbatlas`):
Running Prowler requires specifying the provider (e.g. `aws`, `gcp`, `azure`, `kubernetes`, `m365`, `github`, `iac` or `mongodbatlas`):
<Note>
If no provider is specified, AWS is used by default for backward compatibility with Prowler v2.
+1 -1
View File
@@ -22,7 +22,7 @@ Google Cloud Security Command Center (Cloud SCC) is a centralized security and r
- **GCP-Centric:** While Cloud SCC is powerful within the GCP ecosystem, it is primarily focused on GCP and does not natively extend to multi-cloud environments without additional tools or connectors.
- **Cost Considerations:** As a managed service within GCP, costs can scale with the amount of data ingested and the complexity of the environment, especially as additional features or higher volumes of data are utilized.
- **Cost Considerations:** As a managed service within GCP, costs can scale with the amount of data ingested and the complexity of the environment, especially as additional features or higher volumes of data are used.
- **Dependency on GCP Services:** Cloud SCC's capabilities depend on other GCP services being enabled, such as Security Health Analytics and Web Security Scanner, which may increase overall complexity and cost.
@@ -128,12 +128,12 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.38.0"
PROWLER_API_VERSION="5.38.0"
PROWLER_UI_VERSION="5.40.0"
PROWLER_API_VERSION="5.40.0"
```
<Note>
You can find the latest versions of Prowler Local Server in the [Releases Github section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
You can find the latest versions of Prowler Local Server in the [Releases GitHub section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
</Note>
@@ -45,7 +45,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler i
_Requirements_:
* Have `docker` installed: https://docs.docker.com/get-docker/.
* In the command below, change `-v` to your local directory path in order to access the reports.
* In the command below, change `-v` to your local directory path to access the reports.
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
@@ -161,7 +161,7 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
- "What authentication methods does Prowler support for Azure?"
- "How can I contribute with a new security check to Prowler?"
### Example: Creating a custom dashboard with Prowler extracted data
### Example: Creating a Custom Dashboard with Prowler Extracted Data
In the next example you can see how to create a dashboard using Prowler MCP Server and Claude Desktop.
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

+1
View File
@@ -92,6 +92,7 @@ li[id="/user-guide/tutorials/prowler-alerts"] a > div > div > span:first-child::
li[id="/user-guide/tutorials/prowler-app-attack-paths-active-queries"] a > div > div > span:first-child::after,
li[id="/user-guide/tutorials/prowler-app-findings-triage"] a > div > div > span:first-child::after,
li[id="/user-guide/tutorials/prowler-app-scan-configuration"] a > div > div > span:first-child::after,
li[id="/user-guide/tutorials/prowler-app-slack-integration"] a > div > div > span:first-child::after,
li[id="/user-guide/tutorials/prowler-cloud-aws-organizations"] a > div > div > span:first-child::after,
li[id="/user-guide/tutorials/prowler-cloud-azure-management-groups"] a > div > div > span:first-child::after,
li[id="/user-guide/tutorials/prowler-cloud-gcp-organizations"] a > div > div > span:first-child::after,
+1 -1
View File
@@ -22,7 +22,7 @@ See section [Logging](/user-guide/cli/tutorials/logging) for further information
Common issues with the Docker Compose installation of Prowler Local Server.
### Problem adding AWS Provider using "Connect assuming IAM Role" in Docker
### Problem Adding AWS Provider Using "Connect assuming IAM Role" in Docker
See [GitHub Issue #7745](https://github.com/prowler-cloud/prowler/issues/7745) for more details.
@@ -51,6 +51,7 @@ The following list includes all the AWS checks with configurable variables that
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_actions` | List of Strings | See `config.yaml` |
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_minutes` | Integer | `1440` |
| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_threshold` | Float | `0.2` |
| `cloudwatch_log_group_agentcore_data_protection_policy_enabled` | `agentcore_log_group_name_prefixes` | List of Strings | See `config.yaml` |
| `cloudwatch_log_group_no_secrets_in_logs` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `cloudwatch_log_group_retention_policy_specific_days_enabled` | `log_group_retention_days` | Integer | `365` |
| `codebuild_project_no_secrets_in_variables` | `excluded_sensitive_environment_variables` | List of Strings | `[]` |
@@ -495,7 +496,7 @@ aws:
# AWS CloudTrail Configuration
# aws.cloudtrail_threat_detection_privilege_escalation
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.2 (20%)
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is a privilege_escalation attack event, by default is 0.2 (20%)
threat_detection_privilege_escalation_minutes: 1440 # Past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
threat_detection_privilege_escalation_actions:
[
@@ -6,7 +6,7 @@ In certain organizations, the severity of specific checks might differ from the
The custom metadata option offers a means to override default metadata set by Prowler.
You can utilize `--custom-checks-metadata-file` followed by the path to your custom checks metadata YAML file.
You can use `--custom-checks-metadata-file` followed by the path to your custom checks metadata YAML file.
## Available Fields
+1 -1
View File
@@ -99,7 +99,7 @@ def get_table(data):
## S3 Integration
If you are using Prowler Cloud with the S3 integration or that integration from Prowler CLI and you want to use your data from your S3 bucket, you can run the following command in order to load the dashboard with the new files:
If you are using Prowler Cloud with the S3 integration or that integration from Prowler CLI and you want to use your data from your S3 bucket, you can run the following command to load the dashboard with the new files:
```sh
aws s3 cp s3://<your-bucket>/output/csv ./output --recursive
+1 -1
View File
@@ -105,7 +105,7 @@ def fixer(resource_id: str) -> bool:
return True
```
## Fixer Config file
## Fixer Config File
For some fixers, you can have configurable parameters depending on your use case. You can either use the default config file in `prowler/config/fixer_config.yaml` or create a custom config file and pass it to the fixer with the `--fixer-config` flag. The config file should be a YAML file with the following structure:
+2 -2
View File
@@ -4,7 +4,7 @@ title: 'Miscellaneous'
## Prowler Version
### Showing the Prowler version:
### Showing the Prowler Version
```console
prowler <provider> -V/-v/--version
@@ -22,7 +22,7 @@ To enable verbose mode in Prowler, similar to Version 2, use:
prowler <provider> --verbose
```
### Filter findings by status
### Filter Findings by Status
Prowler allows filtering findings based on their status, ensuring reports and CLI display only relevant findings:
+1 -1
View File
@@ -268,7 +268,7 @@ Accounts:
## AWS Mutelist
### Muting specific AWS regions
### Muting Specific AWS Regions
If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
+1 -4
View File
@@ -43,8 +43,7 @@ prowler <provider> --categories secrets
Several checks analyse resources that are exposed to the Internet, these are:
1. apigateway\_restapi\_public
- apigateway\_restapi\_public
- appstream\_fleet\_default\_internet\_access\_disabled
- awslambda\_function\_not\_publicly\_accessible
- ec2\_ami\_public
@@ -58,8 +57,6 @@ Several checks analyse resources that are exposed to the Internet, these are:
- ecr\_repositories\_not\_publicly\_accessible
- eks\_control\_plane\_endpoint\_access\_restricted
- eks\_endpoints\_not\_publicly\_accessible
- eks\_control\_plane\_endpoint\_access\_restricted
- eks\_endpoints\_not\_publicly\_accessible
- elbv2\_internet\_facing
- kms\_key\_not\_publicly\_accessible
- opensearch\_service\_domains\_not\_publicly\_accessible
+58
View File
@@ -0,0 +1,58 @@
---
title: 'Organizations Across Cloud Providers'
description: 'Understand organization hierarchies and onboarding across AWS, Google Cloud, and Azure'
---
Cloud providers use organization-level hierarchies to group accounts, projects, or subscriptions and apply access and governance consistently. Prowler uses these hierarchies to discover cloud targets and help configure multi-account or multi-project scanning.
This guide explains the shared lifecycle and the differences between AWS Organizations, Google Cloud organizations, and Azure Management Groups. Use the provider-specific guides for commands, permissions, and limitations.
## Organization Lifecycle
Organization-level onboarding generally follows these steps:
1. **Identify the hierarchy:** Locate the organization, management account, management group, folder, organizational unit, or equivalent parent node in the cloud provider.
2. **Grant access:** Assign the provider permissions required to enumerate the hierarchy and read the resources that Prowler scans.
3. **Discover members:** Use Prowler to retrieve accounts, projects, or subscriptions under the selected hierarchy.
4. **Select scan targets:** Choose the cloud targets to connect or scan. Discovery does not necessarily make every discovered target a Prowler provider.
5. **Test access:** Confirm that Prowler can authenticate to each selected target and read its resources.
6. **Scan and maintain:** Run scans, review findings, and repeat discovery when the provider hierarchy changes.
<Note>
Organization membership changes are not automatically synchronized in every Prowler workflow. Follow the provider-specific guide to learn when manual rediscovery is required.
</Note>
## Capability Matrix
| Capability | AWS Organizations | Google Cloud organization | Azure Management Groups |
| --- | --- | --- | --- |
| Hierarchy members | AWS accounts grouped in organizational units (OUs) | Projects grouped in folders and nested folders | Subscriptions grouped in management groups |
| Organization-level discovery | Supported through AWS Organizations APIs | Supported through the Cloud Asset API | Supported through Azure management-group and subscription APIs |
| Primary scan target | AWS account | Google Cloud project | Azure subscription |
| Common organization-level permission | IAM role in the management or delegated administrator account | Cloud Asset Viewer or Cloud Asset Owner at the organization node | Appropriate Azure role assignment at the management-group or subscription scope |
| Provider-specific onboarding | AWS account discovery and optional StackSet role deployment | Project discovery under an organization ID | Subscription discovery under a management group; role assignments inherit to subscriptions |
| Membership maintenance | Repeat the discovery flow when accounts are added or removed | Re-run organization discovery when projects or folders change | Refresh discovery when subscriptions move between management groups |
## Provider Guides
### AWS Organizations
The [AWS Organizations guide](/user-guide/providers/aws/organizations) covers account details, delegated administration, IAM roles, CloudFormation StackSets, and CLI scanning. For Prowler Cloud onboarding, see [AWS Organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-aws-organizations).
### Google Cloud Organization
The [Google Cloud organization guide](/user-guide/providers/gcp/organization) covers scanning projects under an organization ID, organization-level permissions, and Cloud Asset API requirements. For Prowler Cloud onboarding, see [Google Cloud organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-gcp-organizations).
### Azure Management Groups
The [Azure Management Groups guide](/user-guide/providers/azure/management-groups) covers hierarchy setup, role assignment, subscription scope, and Azure-specific limitations. For Prowler Cloud onboarding, see [Azure Management Groups in Prowler Cloud](/user-guide/tutorials/prowler-cloud-azure-management-groups).
## Scope Boundaries
The organization concepts in this guide refer only to cloud-provider resource hierarchies:
- **GitHub organizations** group repositories and GitHub resources. They are a separate provider concept and are not part of AWS, Google Cloud, or Azure organization discovery.
- **MongoDB Atlas organizations** group Atlas projects and teams. They use a separate provider API and authentication model.
- **Prowler Cloud organizations** are internal tenants that isolate providers, scans, findings, users, and permissions. They are not the same as a cloud-provider organization and do not replace one.
Choose the guide that matches the hierarchy being configured, then use the relevant Prowler Cloud or CLI workflow for the scan targets.
@@ -144,25 +144,25 @@ prowler alibabacloud --ecs-ram-role RoleName
### Step 2: Run the First Scan
#### Scan all regions
#### Scan All Regions
```bash
prowler alibabacloud
```
#### Scan specific regions
#### Scan Specific Regions
```bash
prowler alibabacloud --region cn-hangzhou cn-shanghai
```
#### Run specific checks
#### Run Specific Checks
```bash
prowler alibabacloud --checks ram_no_root_access_key ram_user_mfa_enabled_console_access
```
#### Run a compliance framework
#### Run a Compliance Framework
```bash
prowler alibabacloud --compliance cis_2.0_alibabacloud
@@ -64,7 +64,7 @@ This method grants permanent access and is the recommended setup for production
7. Click "Submit" to deploy the stack
![Click on submit](/images/providers/submit-third-page.png)
![Click Submit](/images/providers/submit-third-page.png)
</Tab>
<Tab title="Terraform">
To provision the scan role using Terraform:
@@ -2,7 +2,7 @@
title: 'Scanning Multiple AWS Accounts with Prowler'
---
Prowler enables security scanning across multiple AWS accounts by utilizing the [Assume Role feature](/user-guide/providers/aws/role-assumption) and [integration with AWS Organizations feature](/user-guide/providers/aws/organizations).
Prowler enables security scanning across multiple AWS accounts by using the [Assume Role feature](/user-guide/providers/aws/role-assumption) and [integration with AWS Organizations feature](/user-guide/providers/aws/organizations).
This approach allows execution from a single account with permissions to assume roles in the target accounts.
@@ -12,6 +12,8 @@ See [AWS Organizations](/user-guide/tutorials/prowler-cloud-aws-organizations) i
Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally.
For the cross-provider organization lifecycle and capability comparison, see [Organizations Across Cloud Providers](/user-guide/organizations).
When trusted access is enabled with the Organization, Prowler can discover accounts as they are created and even automate deployment of the Prowler Scan IAM Role.
> ℹ️ Trusted access can be enabled in the Management Account from the AWS Console under **AWS Organizations → Settings → Trusted access for AWS CloudFormation StackSets**.
@@ -165,7 +167,7 @@ Include the `ExternalId` parameter in the StackSet if required by the organizati
When encountering issues during deployment or needing to target specific OUs or environments (e.g., dev/staging/prod), reach out to the Prowler team via [Slack Community](https://prowler.com/slack) or [Support](mailto:support@prowler.com).
## Extra: Run Prowler across all accounts in AWS Organizations by assuming roles
## Extra: Run Prowler Across All Accounts in AWS Organizations by Assuming Roles
### Running Prowler Across All AWS Organization Accounts
@@ -0,0 +1,53 @@
---
title: 'Azure Management Groups in Prowler'
---
Azure Management Groups provide a hierarchy above subscriptions. They allow Azure role assignments and governance policies to apply to multiple subscriptions through a shared scope.
For the cross-provider concepts and lifecycle, see [Organizations Across Cloud Providers](/user-guide/organizations).
## Azure Hierarchy
Azure resources are organized in the following order:
1. Tenant
2. Management groups
3. Subscriptions
4. Resource groups
5. Resources
Prowler scans Azure subscriptions. Management groups help organize those subscriptions and provide a scope where permissions can be assigned, but a management group is not itself a scan target.
## Create a Management Group
To create a management group, follow the [official Azure guide](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal).
![Create management group](/images/create-management-group.gif)
After creating the management group, add the subscriptions that Prowler should access and scan.
![Add Subscription to Management Group](/images/add-sub-to-management-group.gif)
## Assign Roles
Assign the roles required by Prowler at the management-group scope instead of assigning them separately to every subscription. Role assignments at a management group can inherit to its child subscriptions, subject to Azure role-assignment and inheritance rules.
Use the [subscription scope permissions](/user-guide/providers/azure/authentication#subscription-scope-permissions) guide to identify the permissions required for scans. The identity used by Prowler must be able to read the management-group hierarchy and access each subscription selected for scanning.
## Subscription Scope
Management groups organize subscriptions, but Azure scan results remain scoped to individual subscriptions:
- Prowler Cloud scans one subscription per scan.
- Prowler CLI can scan multiple subscriptions by using the `--subscription-ids` option.
- A subscription must be accessible to the configured identity before Prowler can scan it.
- Moving a subscription between management groups can change the permissions it inherits and may require a connection test or rediscovery.
See [Azure Subscription Scope](/user-guide/providers/azure/subscriptions) for subscription selection and CLI options.
## Limitations
- Management groups do not replace subscription providers in Prowler.
- Azure role inheritance depends on the management-group hierarchy and the scope of each assignment; verify access on every subscription selected for scanning.
- The Prowler Cloud workflow is designed around Azure management-group discovery and subscription onboarding. The Prowler CLI workflow still requires explicit subscription selection when restricting scans.
- Changes to management-group membership or role assignments may not be reflected until the hierarchy is refreshed and access is tested again.
@@ -25,14 +25,4 @@ Check the [Authentication > Subscription Scope Permissions](/user-guide/provider
## Recommendation for Managing Multiple Subscriptions
Scanning multiple subscriptions requires creating and assigning roles for each, which can be a time-consuming process. To streamline subscription management and auditing, use management groups in Azure. This approach allows Prowler to efficiently organize and audit multiple subscriptions collectively.
1. **Create a Management Group**: Follow the [official guide](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal) to create a new management group.
![Create management group](/images/create-management-group.gif)
2. **Assign Roles**: Assign necessary roles to the management group, similar to the [role assignment process](#assigning-permissions-for-subscription-scans).
Role assignment should be done at the management group level instead of per subscription.
3. **Add Subscriptions**: Add all subscriptions you want to audit to the newly created management group. ![Add Subscription to Management Group](/images/add-sub-to-management-group.gif)
Scanning multiple subscriptions requires creating and assigning roles for each, which can be a time-consuming process. To streamline subscription management and auditing, use [Azure Management Groups](/user-guide/providers/azure/management-groups) to organize subscriptions and assign permissions collectively.
@@ -4,6 +4,8 @@ title: 'Scanning a Specific GCP Organization'
By default, Prowler scans all Google Cloud projects accessible to the authenticated user.
For the cross-provider organization lifecycle and capability comparison, see [Organizations Across Cloud Providers](/user-guide/organizations).
To limit the scan to projects within a specific Google Cloud organization, use the `--organization-id` option with the GCP organization’s ID:
```console
@@ -42,7 +42,7 @@ Required for scanning repository security settings:
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Branch protection, security settings | All branch protection checks, secret scanning status |
| **Administration** | Read | Branch protection, security and Actions settings | All branch protection checks, secret scanning status, `repository_default_workflow_permissions_read_only` |
| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` |
| **Metadata** | Read | Basic repository information | All checks (automatically granted) |
| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` |
@@ -63,7 +63,7 @@ Required for scanning organization-level security settings:
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` |
| **Administration** | Read | Organization security policies and Actions settings | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict`, `organization_default_workflow_permissions_read_only`, `organization_actions_pull_request_approval_disabled` |
| **Members** | Read | Member access reviews | Organization membership auditing |
#### Account Permissions (Fine-Grained PAT only)
@@ -83,8 +83,8 @@ With the **Read-only permissions** listed above, Prowler can run:
| Check Category | Coverage | Notes |
|----------------|----------|-------|
| Branch protection checks (12 checks) | ✅ Full | Signed commits, status checks, PR reviews, etc. |
| Repository security checks | ✅ Full | Secret scanning, Dependabot, SECURITY.md, CODEOWNERS |
| Organization checks (3 checks) | ✅ Full | MFA, repo creation policies, default permissions |
| Repository security checks | ✅ Full | Secret scanning, Dependabot, SECURITY.md, CODEOWNERS, default workflow permissions |
| Organization checks (5 checks) | ✅ Full | MFA, repo creation policies, default permissions, Actions workflow permissions |
| Compliance frameworks | ✅ Full | CIS GitHub Benchmark and others |
| Merge settings (`delete_branch_on_merge`) | ⚠️ MANUAL | Requires write permission (see below) |
@@ -171,6 +171,7 @@ Use OAuth App Tokens when building applications that need delegated user permiss
- `repo`: Full control of repositories
- `read:org`: Read organization and team membership
- `admin:org`: Required by `organization_default_workflow_permissions_read_only` and `organization_actions_pull_request_approval_disabled` to read the organization Actions workflow permissions
- `read:user`: Read user profile data
**Create an OAuth App:**
@@ -214,7 +215,7 @@ If a GitHub App is required:
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Branch protection, security settings | All branch protection checks, `repository_secret_scanning_enabled` |
| **Administration** | Read | Branch protection, security and Actions settings | All branch protection checks, `repository_secret_scanning_enabled`, `repository_default_workflow_permissions_read_only` |
| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` |
| **Metadata** | Read | Basic repository information | All checks (automatically granted) |
| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` |
@@ -223,7 +224,7 @@ If a GitHub App is required:
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` |
| **Administration** | Read | Organization security policies and Actions settings | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict`, `organization_default_workflow_permissions_read_only`, `organization_actions_pull_request_approval_disabled` |
| **Members** | Read | Member access reviews | Organization membership auditing |
**Create a GitHub App:**
@@ -306,9 +306,21 @@ prowler image --registry internal-registry.local --registry-insecure
```
<Warning>
Skipping TLS verification disables certificate validation for registry connections. Use this flag only for trusted internal registries with self-signed certificates.
Skipping TLS verification disables certificate validation for registry connections, including the Trivy image pull (`TRIVY_INSECURE`). Use this flag only for trusted internal registries with self-signed certificates.
</Warning>
#### On-Premises Registries and Private Networks
<VersionBadge version="5.41.0" />
By default, Prowler rejects registry-provided URLs (token endpoints, pagination links) that resolve to non-public addresses, as an SSRF defense. On-premises registries live on private networks by definition, so to scan them declare the trusted ranges explicitly:
```bash
export PROWLER_IMAGE_PROVIDER_ALLOWED_PRIVATE_NETWORKS="192.168.65.254/32,10.20.0.0/16"
```
The value is a comma-separated list of IPs and CIDRs. A resolved address inside an allowlisted range is permitted; every other non-public address stays blocked, so link-local (`169.254.169.254`), loopback, and the rest of the internal network remain protected. The variable applies to registry enumeration and to the connection test. Malformed entries fail at startup, and a non-empty allowlist is logged as a relaxed security control. When unset, behavior is unchanged: only public addresses are followed.
#### Supported Registries
Registry Scan Mode supports the following registry types:
@@ -36,7 +36,7 @@ If **Require IP Access List for the Atlas Administration API** is enabled in the
<VersionBadge version="5.15.0" />
### Step 1: Add the provider
### Step 1: Add the Provider
1. Navigate to **Providers** and click **Add Provider**.
![Add provider list](./img/add-provider-list.png)
@@ -45,13 +45,13 @@ If **Require IP Access List for the Atlas Administration API** is enabled in the
![Add organization ID](./img/add-org-id.png)
4. (Optional) Add a friendly alias to identify this organization in dashboards.
### Step 2: Provide API credentials
### Step 2: Provide API Credentials
1. Click **Next** to open the credentials form.
2. Paste the **Atlas Public Key** and **Atlas Private Key** generated in the Atlas console.
![Add credentials](./img/add-credentials.png)
### Step 3: Test the connection and start scanning
### Step 3: Test the Connection and Start Scanning
1. Click **Test connection** to ensure Prowler Cloud can reach the Atlas API.
2. Save the credentials. The provider will appear in the list with its current connection status.
@@ -66,11 +66,11 @@ If **Require IP Access List for the Atlas Administration API** is enabled in the
You can also run MongoDB Atlas assessments directly from the CLI. Both command-line flags and environment variables are supported.
### Step 1: Select an authentication method
### Step 1: Select an Authentication Method
Choose one of the following authentication methods:
#### Command-line arguments
#### Command-Line Arguments
```bash
prowler mongodbatlas \
@@ -78,7 +78,7 @@ prowler mongodbatlas \
--atlas-private-key <private_key>
```
#### Environment variables
#### Environment Variables
```bash
export ATLAS_PUBLIC_KEY=<public_key>
@@ -86,9 +86,9 @@ export ATLAS_PRIVATE_KEY=<private_key>
prowler mongodbatlas
```
### Step 2: Run the first scan
### Step 2: Run the First Scan
#### Scan all projects and clusters
#### Scan All Projects and Clusters
```bash
prowler mongodbatlas
@@ -96,7 +96,7 @@ prowler mongodbatlas
This command enumerates all projects accessible to the API key and scans every cluster.
#### Scan a specific project
#### Scan a Specific Project
Add the `--atlas-project-id` flag when you only want to assess one project:
@@ -104,7 +104,7 @@ Add the `--atlas-project-id` flag when you only want to assess one project:
prowler mongodbatlas --atlas-project-id <project-id>
```
### Additional tips
### Additional Tips
- Combine flags (for example, `--checks` or `--services`) just like with other providers.
- Use `--output-modes` to export findings in JSON, CSV, ASFF, etc.
@@ -67,7 +67,7 @@ The service application must be assigned **one** of the following Okta admin rol
Okta's Management API enforces a two-layer authorization model: an OAuth **scope** decides which API endpoints the token can call, and an **admin role** decides whether the call returns data. With only a scope granted, the token mint succeeds but every read returns `403 Forbidden`. Read-Only Administrator is the minimum role that lets the granted `okta.*.read` scopes return configuration data to Prowler's checks; without it, the credential probe at provider startup fails and the scan never gets to evaluate any check.
#### When Super Administrator is required
#### When Super Administrator Is Required
Four checks need to resolve the Authentication Policy bound to Okta's first-party apps (Okta Admin Console, Okta Dashboard) and depend on `/api/v1/apps` returning those system apps — which Okta restricts to Super Administrator:
@@ -92,17 +92,17 @@ Read-Only Administrator stays the recommended default for the least-privilege fr
## Step-by-Step Setup
### 1. Go to the admin console
### 1. Go to the Admin Console
![Okta — admin console page](/user-guide/providers/okta/images/select-admin-console.png)
### 2. [Optional] - Disable the privilege-escalation bypass (org-wide, one-time)
### 2. [Optional] - Disable the Privilege-Escalation Bypass (Org-Wide, One-Time)
In the Okta Admin Console, go to **Settings → Account → Public client app admins** and ensure it is **off**. When enabled, every API Services app can be auto-assigned the Super Administrator role after scopes are granted, which would invalidate the read-only premise of this integration.
![Okta — disable Public client app admins](/user-guide/providers/okta/images/public-client-app-admins.png)
### 3. Create the API Services app
### 3. Create the API Services App
1. Go to **Applications → Applications**.
@@ -118,7 +118,7 @@ In the Okta Admin Console, go to **Settings → Account → Public client app ad
![Okta — copy client id](/user-guide/providers/okta/images/copy-client-id.png)
### 4. Switch to private-key authentication and generate a keypair
### 4. Switch to Private-Key Authentication and Generate a Keypair
On the new app's **General** tab, scroll to **Client Credentials**:
@@ -136,13 +136,13 @@ Okta displays the private key **only once**. If you close the modal without copy
![Okta — create Public Key](/user-guide/providers/okta/images/create-public-key.png)
### 5. Grant the required OAuth scopes
### 5. Grant the Required OAuth Scopes
On the app, open the **Okta API Scopes** tab and click **Grant** on every scope Prowler needs. The bundled checks require `okta.policies.read`, `okta.brands.read`, `okta.apps.read`, `okta.authenticators.read`, `okta.networkZones.read`, `okta.apiTokens.read`, `okta.roles.read`, `okta.groups.read`, `okta.logStreams.read`, and `okta.idps.read`.
![Okta — grant OAuth scopes](/user-guide/providers/okta/images/grant-permissions.png)
### 6. Assign an admin role
### 6. Assign an Admin Role
On the app, open the **Admin roles** tab and click **Edit assignments → Add assignment**:
@@ -155,7 +155,7 @@ To additionally evaluate the first-party application checks (Okta Admin Console
![Okta — grant Read-Only role](/user-guide/providers/okta/images/grant-roles.png)
### 7. [Optional] Verify DPoP setting
### 7. [Optional] Verify DPoP Setting
Prowler sends DPoP (Demonstrating Proof of Possession) proofs on every token request. The integration works whether the **Require Demonstrating Proof of Possession (DPoP) header in token requests** setting on the service app is on or off — but enabling it is the more secure default.
@@ -206,20 +206,20 @@ The org domain must be `<org>.okta.com` (or `.oktapreview.com` / `.okta-emea.com
The file at `OKTA_PRIVATE_KEY_FILE` is missing, unreadable, or empty. Confirm the path and that the file contains a non-empty PEM block or JWK JSON document.
### `OktaInvalidCredentialsError` at provider init
### `OktaInvalidCredentialsError` at Provider Init
Prowler validates credentials at startup by listing one sign-on policy. This error indicates the credential material itself was rejected:
- **`invalid_client`** — the public key registered in Okta does not match the private key on disk. Generate a fresh keypair and try again.
### `OktaInsufficientPermissionsError` at provider init
### `OktaInsufficientPermissionsError` at Provider Init
Raised when the credential probe succeeds at the OAuth layer but the request is rejected because the service app lacks the required scope or admin role:
- **`invalid_scope`** — one of the requested scopes (`okta.policies.read`, `okta.brands.read`, `okta.apps.read`, `okta.authenticators.read`, `okta.networkZones.read`, `okta.apiTokens.read`, `okta.roles.read`, `okta.groups.read`, `okta.logStreams.read`, and `okta.idps.read`) is not granted on the service app. Grant the missing scope from **Okta API Scopes**.
- **`Forbidden` / `not authorized`** — no admin role is assigned to the service app. Assign **Read-Only Administrator** (or **Super Administrator** for the first-party application checks) from **Admin roles**.
### Application-service checks return MANUAL on first-party apps
### Application-Service Checks Return MANUAL on First-Party Apps
When the service app runs with Read-Only Administrator, the five application-service checks targeting the Okta Admin Console and Okta Dashboard return MANUAL. This is by design — Okta restricts the underlying endpoints (`/api/v1/first-party-app-settings/{appName}` and `/api/v1/apps` for first-party app `name` values `saasure` / `okta_enduser`) to **Super Administrator**. Assign the Super Administrator role to the service app to evaluate those checks. See [Required Admin Role](#required-admin-role) for the full list.
@@ -34,4 +34,4 @@ prowler scaleway
## Required Scaleway Permissions
The API key bearer needs read access to the IAM API in order to list users and API keys. The `IAMReadOnly` policy is sufficient. Refer to the [Scaleway IAM policy reference](https://www.scaleway.com/en/docs/identity-and-access-management/iam/reference-content/permission-sets/) for the full list of permissions.
The API key bearer needs read access to the IAM API to list users and API keys. The `IAMReadOnly` policy is sufficient. Refer to the [Scaleway IAM policy reference](https://www.scaleway.com/en/docs/identity-and-access-management/iam/reference-content/permission-sets/) for the full list of permissions.
@@ -13,7 +13,7 @@ StackIT support in Prowler is community-maintained. For commercial support or to
Before running Prowler with the StackIT provider, ensure you have:
1. A StackIT account with at least one project
2. A StackIT service account key file with permissions on the project (`iaas.viewer` is enough for the currently shipped IaaS checks; `project.owner` works for any future service). See the [Authentication guide](/user-guide/providers/stackit/authentication) for the full setup.
2. A StackIT service account key file with project permissions for the services to scan. `iaas.viewer` is sufficient for the currently shipped IaaS checks. For SKE, assign the project-scoped `Reader` role as a broad read-only convenience, or create a custom role with `resource-manager.project.get`, `ske.cluster.list`, `ske.cluster.get`, and `ske.version.list`. The `resource-manager.project.get` permission is optional for scanning; it only lets Prowler verify and display the project name. See the [Authentication guide](/user-guide/providers/stackit/authentication) for the full setup.
3. Access to Prowler CLI (see [Installation](/getting-started/installation/prowler-cli))
## Prowler CLI
@@ -124,7 +124,9 @@ Prowler outputs findings to the console and writes reports to the `output/` dire
| Service | StackIT API | Description | Example Checks |
|---------|-------------|-------------|----------------|
| **IaaS** | `iaas` | Virtual machines, network interfaces, security groups | `iaas_security_group_ssh_unrestricted`, `iaas_security_group_rdp_unrestricted`, `iaas_security_group_database_unrestricted`, `iaas_security_group_all_traffic_unrestricted` |
| **IaaS** | `iaas` | Virtual machines, network interfaces, security groups | `iaas_security_group_ssh_unrestricted`, `iaas_security_group_rdp_unrestricted`, `iaas_security_group_database_unrestricted`, `iaas_security_group_all_traffic_unrestricted`, `iaas_server_public_ip_attached` |
| **Object Storage** | `objectstorage` | Buckets, object lock, retention policies, access keys | `objectstorage_bucket_object_lock_enabled`, `objectstorage_bucket_retention_policy`, `objectstorage_access_key_expiration` |
| **Kubernetes Engine** | `ske` | Managed Kubernetes clusters | `ske_cluster_no_public_endpoint` |
Additional services will be added in future releases. Track progress in the [Prowler release notes](https://github.com/prowler-cloud/prowler/releases).
@@ -136,6 +138,8 @@ If the scan fails with a 401 error, the service account key is no longer valid (
### Permission Errors
If checks fail with a 403 error, the service account is missing the required role on the project. Re-check the role assignment in the StackIT portal (`iaas.viewer` is the minimum for the shipped IaaS checks).
If the connection probe warns that Resource Manager access could not be verified with a 403 response, Prowler continues to service discovery. Grant the optional `resource-manager.project.get` permission to verify and display the project name.
If a service call fails with a 403 response, the service account is missing a permission required by that service. `iaas.viewer` is sufficient for the shipped IaaS checks. The `ske_cluster_no_public_endpoint` check lists clusters and therefore requires `ske.cluster.list`; STACKIT least-privilege custom-role examples also include `ske.cluster.get` and `ske.version.list`. Service API calls remain authoritative and stop the scan when access is denied.
For detailed setup steps, see the [Authentication guide](/user-guide/providers/stackit/authentication).
@@ -4,6 +4,8 @@ title: 'AWS Organizations Bulk Provisioning in Prowler'
Prowler offers an automated tool to discover and provision all AWS accounts within an AWS Organization. This streamlines onboarding for organizations managing multiple AWS accounts by automatically generating the configuration needed for bulk provisioning.
For the cross-provider organization lifecycle and terminology, see [Organizations Across Cloud Providers](/user-guide/organizations).
The tool, `aws_org_generator.py`‎, complements the [Bulk Provider Provisioning](./bulk-provider-provisioning) tool and is available in the Prowler repository at: [util/prowler-bulk-provisioning](https://github.com/prowler-cloud/prowler/tree/master/util/prowler-bulk-provisioning)
<Note>
+46 -6
View File
@@ -1,7 +1,7 @@
---
title: 'Alerts'
sidebarTitle: 'Alerts'
description: 'Create email alerts from Prowler Cloud findings to monitor relevant security changes after scans or in daily digests.'
description: 'Create alerts from Prowler Cloud findings, deliver them to email recipients and Slack channels, and monitor relevant security changes after scans or in daily digests.'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
@@ -9,7 +9,7 @@ import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
<VersionBadge version="5.26.0" />
Alerts notify recipients by email when security findings match saved filter conditions. Use Alerts to track high-priority findings, monitor specific providers or services, and keep teams informed about scan results that match defined criteria.
Alerts notify their destinations — email recipients, Slack channels, or both — when security findings match saved filter conditions. Use Alerts to track high-priority findings, monitor specific providers or services, and keep teams informed about scan results that match defined criteria.
<SubscriptionBanner />
@@ -19,12 +19,13 @@ Before creating Alerts, ensure that:
* At least one scan has completed and produced findings.
* The user role includes the `manage_alerts` permission.
* To deliver Alerts to Slack channels, a Slack workspace is connected, at least one channel is authorized on it, and the integration's connection check has confirmed that channel. See [Slack Integration](/user-guide/tutorials/prowler-app-slack-integration).
The `manage_alerts` permission is required to create, edit, test, enable, disable, and delete Alerts. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details.
## How Alerts Work
Alerts are created from Findings filters. When an Alert runs, Prowler Cloud evaluates the saved conditions against findings and sends an email digest when matching findings exist.
Alerts are created from Findings filters. When an Alert runs, Prowler Cloud evaluates the saved conditions against findings and notifies the Alert's destinations when matching findings exist: an email digest to each recipient, a message to each Slack channel, or both. Destination kinds are independent — neither requires the other, and neither displaces the other.
<Note>
Alerts evaluate findings with status `FAIL` only. Findings with status `PASS` or `MANUAL`, and muted findings, never trigger an Alert regardless of the saved filters.
@@ -53,6 +54,7 @@ To create an Alert:
* **Description:** Add optional context for the Alert.
* **Frequency:** Select when Prowler Cloud should evaluate the Alert.
* **Recipients:** Select the recipients who should receive the email digest.
* **Destination channels:** Select the Slack channels that should receive the Alert. See [Slack Channel Destinations](#slack-channel-destinations).
![Create Alert Modal](/images/prowler-app/alerts/create-alert-modal.png)
@@ -86,11 +88,18 @@ Navigate to **Alerts** to review and manage existing Alerts.
![Alerts List](/images/prowler-app/alerts/alerts-list.png)
The **Destinations** column summarizes where each Alert delivers, without the Alert being opened:
* **Email recipients:** The first address, plus a count of the rest, such as `security@example.com +2 more`.
* **Slack channels:** The first channel, plus a count of the rest, such as `#sec-alerts +1 more`.
Each summary is omitted when that destination kind is empty, and the column reads **No destinations** when an Alert has neither.
Each Alert provides these actions:
| Action | Description |
|--------|-------------|
| Edit | Update name, description, recipients, frequency, or filters. |
| Edit | Update name, description, recipients, Slack channels, frequency, or filters. |
| Enable/Disable | Start or stop Alert evaluation without deleting the Alert. |
| Delete | Permanently remove the Alert. |
@@ -125,6 +134,37 @@ By default, the **organization owner** receives a **daily digest** for **critica
If a recipient unsubscribes from Alerts, that address stops receiving digests until it is reconfirmed.
An Alert does not require email recipients: an Alert that targets Slack channels only is accepted with those channels as its sole destinations. An Alert with no destinations at all stays valid and keeps evaluating its filters, but it delivers nothing.
## Slack Channel Destinations
<VersionBadge version="5.40.0" />
An Alert can post to Slack channels alongside its email recipients, or instead of them. The **Destination channels** field sits directly below **Recipients** in the Alert form, both when creating an Alert and when editing one. When the Alert matches findings, Prowler Cloud posts a message to each of its channels and sends the email digest to each of its recipients, independently of each other.
The channels offered are the confirmed channels of the connected Slack integration, never the whole Slack workspace. Widening the pool takes two steps on the integration: authorize the channel there, then run its connection check, which confirms the channel by posting a one-time confirmation message to it. Once confirmed, the channel is selectable on every Alert. See [Slack Integration](/user-guide/tutorials/prowler-app-slack-integration) for connecting a workspace, authorizing its channels, and confirming them.
A channel that was authorized a moment ago but does not appear in the Alert form has not been confirmed yet. Run **Test connection** on the Slack integration, then reopen the Alert form.
Private channels are identified as **Private** both in the open channel list and on the selected channels once the list is closed, so a private destination is never mistaken for a public one.
### When Slack Channels Cannot Be Selected
The field is always present, so channel delivery is never silently missing. It reports why it cannot be used:
| State | What the Alert form shows |
|-------|---------------------------|
| No Slack workspace connected | The field is visible but cannot be edited, explaining that posting Alerts to Slack channels needs a connected Slack workspace, with a link to the Slack integration. |
| Workspace connected, no confirmed channels | A notice that no channels are available yet and that they are authorized and confirmed on the Slack integration, with the same link. |
In both states the rest of the Alert is unaffected: it can still be created or saved with its filters, frequency, and email recipients.
<Note>
Slack destinations stay in step with the integration. Removing a channel from the integration's authorized set — or disconnecting the Slack integration altogether — removes that channel from every Alert that targeted it, so an Alert never keeps a destination Prowler can no longer deliver to. The Alert keeps its filters, frequency, and email recipients, and future delivery to that channel stops: nothing is posted to announce the removal, and the notifications already delivered stay in the channel. Restoring delivery means authorizing and confirming the channel again on the integration, then selecting it again on the Alert.
</Note>
Saving an Alert that names a channel which is not a confirmed channel of a connected Slack integration is refused, and the reason is reported on the Alert form. Authorize and confirm the channel on the Slack integration, or remove it from the Alert, and save again.
## Email Notifications
When an Alert matches findings, Prowler Cloud sends a security alert email that summarizes the matching findings. The email includes:
@@ -141,6 +181,6 @@ When an Alert matches findings, Prowler Cloud sends a security alert email that
* **Start with focused filters:** Create Alerts for specific high-priority scopes, such as critical findings, production providers, or important services.
* **Use clear names:** Choose names that explain the intent of the Alert.
* **Review recipients regularly:** Keep recipient lists aligned with current ownership.
* **Review destinations regularly:** Keep recipient lists and channel selections aligned with current ownership.
* **Test before saving edits:** Use **Test** after changing filters to confirm that the Alert matches the expected findings.
* **Disable instead of deleting during tuning:** Disable Alerts temporarily when adjusting filters or recipients.
* **Disable instead of deleting during tuning:** Disable Alerts temporarily when adjusting filters or destinations.
@@ -16,26 +16,24 @@ Attack Paths analyzes relationships between cloud resources, permissions, and se
By mapping these relationships as a graph, Attack Paths reveals risks that individual security checks cannot detect on their own, such as an IAM role that can escalate its own permissions, or a chain of policies that grants unintended access to sensitive resources.
<Note>
Attack Paths is currently available for **AWS** providers. Support for
additional providers is planned.
Attack Paths is currently available for **AWS** providers. Support for additional providers is planned.
</Note>
## Prerequisites
The following prerequisites are required for Attack Paths:
- **An AWS provider is configured** with valid credentials in Prowler Cloud. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
- **An AWS provider is configured** with valid credentials. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
- **At least one scan has completed** on the configured AWS provider and produced graph data. Attack Paths scans run automatically alongside regular security scans, no separate configuration is required.
## How Attack Paths Scans Work
Attack Paths scans are generated automatically when a security scan runs on an AWS provider. Each completed scan produces graph data that maps relationships between IAM principals, policies, trust configurations, and other resources.
Attack Paths scans are generated automatically when a security scan runs on an AWS provider. When a scan produces graph data, it maps relationships between IAM principals, policies, trust configurations, and other resources. A scan can complete without producing graph data.
Once the scan finishes and graph data is ready, the scan appears in the Attack Paths scan table with a **Completed** status and a check in the **Graph** column. Scans that are still queued or running remain visible, but they cannot be selected until graph data is ready.
When graph data is ready, the scan appears in the Attack Paths scan table with a check in the **Graph** column and can be selected regardless of its current status. Scans without graph data remain visible but cannot be selected. If a new scan cycle starts after graph data is available, the previous cycle remains available while the new scan runs.
<Note>
Since Prowler scans all configured providers every **24 hours** by default,
Attack Paths data stays up to date automatically.
Prowler Cloud and Prowler Private Cloud scan configured providers every **24 hours** by default, so Attack Paths data stays up to date automatically.
</Note>
## Accessing Attack Paths
@@ -66,7 +64,7 @@ The scans table displays all Attack Paths scans with the following columns:
- **Graph:** Whether Attack Paths graph data is available for the scan.
- **Duration:** Total scan time.
To select a scan for analysis, click the radio button on any row with a **Completed** status and available graph data.
To select a scan for analysis, click any row with a check in the **Graph** column. A row can remain selectable while a new scan cycle runs because Attack Paths keeps the graph from the previous completed cycle available.
<img
src="/images/prowler-app/attack-paths/scan-list-table.png"
@@ -75,8 +73,7 @@ To select a scan for analysis, click the radio button on any row with a **Comple
/>
<Note>
Only scans with graph data can be selected. Disabled rows include a tooltip
that explains why the graph is not available yet.
Only scans with graph data can be selected. Disabled rows include a tooltip that explains why the graph is not available yet.
</Note>
## Choosing a Query
@@ -97,9 +94,7 @@ To choose a query, click the dropdown and select from the available options. Eac
Once selected, a description panel appears below the dropdown with more context about the query.
<Note>
In Prowler Cloud and Prowler Private Cloud, the query selector hides queries
confirmed empty for the selected scan, so only queries that return data remain
visible. See [Active Queries](/user-guide/tutorials/prowler-app-attack-paths-active-queries).
In Prowler Cloud and Prowler Private Cloud, the query selector hides built-in queries confirmed empty for the selected scan. Built-in queries without a confirmed empty result and the **Custom openCypher query** remain visible. See [Active Queries](/user-guide/tutorials/prowler-app-attack-paths-active-queries).
</Note>
## Configuring Query Parameters
@@ -120,7 +115,7 @@ For example, **Internet-Exposed EC2 with Sensitive S3 Access** uses **Tag key**
## Writing Custom openCypher Queries
In addition to the built-in queries, Attack Paths supports custom read-only [openCypher](https://opencypher.org/) queries. Custom queries provide direct access to the underlying graph so security teams can answer ad-hoc questions, prototype detections, or extend coverage beyond the built-in catalogue.
In addition to the built-in queries, Attack Paths supports custom read-only [openCypher](https://opencypher.org/) queries. Custom queries provide direct access to the underlying graph so security teams can answer ad-hoc questions, prototype detections, or extend coverage beyond the built-in catalog.
To write a custom query, select **Custom openCypher query** from the query dropdown. A code editor with syntax highlighting and line numbers appears, ready to receive the query.
@@ -192,11 +187,7 @@ Custom queries traverse the same Cartography graph the built-in queries use. Nod
For the complete reference, including the graph model, list-typed and JSON-encoded properties, performance guidance, and openCypher compatibility rules, see [Attack Paths Queries](/developer-guide/attack-paths-queries) in the Developer Guide.
<Note>
AI assistants connected through Prowler MCP Server can fetch the exact
Cartography schema for the active scan via the
`prowler_get_attack_paths_cartography_schema` tool. This guarantees that
generated queries match the schema version pinned by the running Prowler
release.
AI assistants connected through Prowler MCP Server can fetch the exact Cartography schema for the active scan via the `prowler_get_attack_paths_cartography_schema` tool. This guarantees that generated queries match the schema version pinned by the running Prowler release.
</Note>
## Executing a Query
@@ -221,12 +212,22 @@ If the query returns no results, an informational message appears. Common reason
After a successful execution, the graph visualization renders below the query builder. The graph maps relationships between cloud resources, IAM entities, public exposure, and security findings.
### Grouped Graphs and Query Outcomes
<VersionBadge version="5.39.0" />
Prowler Cloud and Prowler Private Cloud group resources of the same class and graph level into expandable nodes. Built-in query graphs also end with a query outcome, which states the result that the path can lead to.
Prowler Local Server keeps the flat graph view, including the provider root. Custom openCypher queries do not have a catalog outcome, so their graphs do not include an outcome node.
### Node Types
- **Provider root nodes:** Represent the AWS account or provider root for the selected scan.
- **Resource nodes:** Represent cloud resources such as IAM roles, policies, EC2 instances, security groups, and S3 buckets.
- **Grouped resource nodes:** Represent multiple resources of the same class in Prowler Cloud and Prowler Private Cloud. A number in the upper-right corner shows how many resources the node contains. A red outline indicates that one or more resources in the group have findings.
- **Resource nodes:** Represent individual cloud resources such as IAM roles, policies, EC2 instances, security groups, and S3 buckets. A class with one resource remains an individual node.
- **Internet nodes:** Represent exposure from the public internet.
- **Finding nodes:** Represent Prowler findings linked to resources. Finding colors indicate risk level, such as critical, high, medium, or low.
- **Outcome nodes:** Mark the terminal result of a built-in query in Prowler Cloud and Prowler Private Cloud. The orange node displays outcomes such as **Code execution**, **Privilege escalation**, **Public exposure**, or **Resource inventory**. A dashed ring and the label **Latent outcome** indicate an inventory or another partial outcome.
- **Provider root nodes:** Represent the AWS account or provider root in the Prowler Local Server flat graph.
### Edge Types
@@ -234,7 +235,7 @@ After a successful execution, the graph visualization renders below the query bu
- **Finding edges:** Dashed relationships between resources and their associated findings.
- **Highlighted paths:** Green edges that show the active path when you hover a node or focus a finding.
The standard graph view includes a minimap and a legend below the canvas. The legend shows the provider roots, visible node types, finding risk levels, node states, and edge types present in the current view.
The standard graph view includes a minimap and a legend below the canvas. The legend shows the visible node types, finding risk levels, node states, and edge types present in the current view. Prowler Local Server also displays the provider root in the legend.
<img
src="/images/prowler-app/attack-paths/graph-visualization.png"
@@ -244,17 +245,32 @@ The standard graph view includes a minimap and a legend below the canvas. The le
## Interacting with the Graph
The graph banner describes the main interactions:
The graph supports these interactions:
- Click a node with a number in the upper-right corner to expand its resource group.
- Click a finding to focus its connected path.
- Click a resource with findings to show or hide its related findings.
- Hover a node to highlight its connected path.
### Expanding Resource Groups
In Prowler Cloud and Prowler Private Cloud, a node with a number in its upper-right corner represents a resource group. The number is the total number of resources in the group.
- Click the group node to display its resources. Multiple groups can remain expanded, and the graph automatically fits the visible nodes to the canvas.
- Double-click any resource revealed from a group to collapse that group.
- Click **Collapse all groups** in the graph toolbar to close every expanded group. This control appears only while at least one group is expanded.
<img
src="/images/prowler-app/attack-paths/graph-visualization-expanded.png"
alt="Attack Paths graph showing an expanded AWS Role group and remaining numbered resource groups"
width="700"
/>
### Showing Related Findings
Resource nodes with related findings are clickable. Click one of these resources to show its finding nodes. Click the resource again to hide them.
The graph automatically fits the selected resource and its related findings when the findings are shown.
The selected resource is highlighted in green while its findings are visible. The graph automatically fits the selected resource and its related findings when the findings are shown.
### Focusing a Finding Path
@@ -279,12 +295,12 @@ The toolbar in the top-right corner of the graph provides:
- **Zoom in / Zoom out:** Adjust the zoom level
- **Fit graph to view:** Reset the view to fit the visible graph
- **Export graph:** Download the current graph as a PNG file
- **Collapse all groups:** Close every expanded resource group. This control appears only when a group is expanded
- **Export graph:** Download the current graph, including its grouped or expanded state and outcome, as a PNG file
- **Fullscreen:** Open the graph in a full-size modal
<Note>
Use **Ctrl + Scroll** (or **Cmd + Scroll** on macOS) to zoom directly within
the graph area.
Use **Ctrl + Scroll** (or **Cmd + Scroll** on macOS) to zoom directly within the graph area.
</Note>
## Viewing Finding Details

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