Merge branch 'master' into DEVREL-98-include-open-api-specification-in-mintlify

This commit is contained in:
Andoni Alonso
2025-11-10 08:39:59 +01:00
committed by GitHub
90 changed files with 4438 additions and 334 deletions
+7 -2
View File
@@ -14,7 +14,11 @@ UI_PORT=3000
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
#### Code Review Configuration ####
# Enable Claude Code standards validation on pre-push hook
# Set to 'true' to validate changes against AGENTS.md standards via Claude Code
# Set to 'false' to skip validation
CODE_REVIEW_ENABLED=true
#### Prowler API Configuration ####
PROWLER_API_VERSION="stable"
@@ -132,4 +136,5 @@ LANGCHAIN_PROJECT=""
# Example with one source:
RSS_FEED_SOURCES='[{"id":"prowler-releases","name":"Prowler Releases","type":"github_releases","url":"https://github.com/prowler-cloud/prowler/releases.atom","enabled":true}]'
# Example with multiple sources (no trailing comma after last item):
# RSS_FEED_SOURCES='[{"id":"prowler-releases","name":"Prowler Releases","type":"github_releases","url":"https://github.com/prowler-cloud/prowler/releases.atom","enabled":true},{"id":"prowler-blog","name":"Prowler Blog","type":"blog","url":"https://prowler.com/blog/rss","enabled":false}]'
# RSS_FEED_SOURCES='[{"id":"prowler-releases","name":"Prowler Releases","type":"github_releases","url":"https://github.com/prowler-cloud/prowler/releases.atom","enabled":true},{"id":"prowler-blog","name":"Prowler Blog","type":"blog","url":"https://prowler.com/blog/rss","enabled":false}]'
@@ -12,7 +12,7 @@ inputs:
required: false
default: ''
step-outcome:
description: 'Outcome of a step to determine status (success/failure) - automatically sets STATUS_EMOJI, STATUS_TEXT, and STATUS_COLOR env vars'
description: 'Outcome of a step to determine status (success/failure) - automatically sets STATUS_TEXT and STATUS_COLOR env vars'
required: false
default: ''
outputs:
@@ -27,16 +27,14 @@ runs:
shell: bash
run: |
if [[ "${{ inputs.step-outcome }}" == "success" ]]; then
echo "STATUS_EMOJI=[✓]" >> $GITHUB_ENV
echo "STATUS_TEXT=succeeded" >> $GITHUB_ENV
echo "STATUS_COLOR=6aa84f" >> $GITHUB_ENV
echo "STATUS_TEXT=Completed" >> $GITHUB_ENV
echo "STATUS_COLOR=#6aa84f" >> $GITHUB_ENV
elif [[ "${{ inputs.step-outcome }}" == "failure" ]]; then
echo "STATUS_EMOJI=[✗]" >> $GITHUB_ENV
echo "STATUS_TEXT=failed" >> $GITHUB_ENV
echo "STATUS_COLOR=fc3434" >> $GITHUB_ENV
echo "STATUS_TEXT=Failed" >> $GITHUB_ENV
echo "STATUS_COLOR=#fc3434" >> $GITHUB_ENV
else
# No outcome provided - pending/in progress state
echo "STATUS_COLOR=dbab09" >> $GITHUB_ENV
echo "STATUS_COLOR=#dbab09" >> $GITHUB_ENV
fi
- name: Send Slack notification (new message)
+7
View File
@@ -22,6 +22,13 @@ Please add a detailed description of how to review this PR.
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/prowler/CHANGELOG.md), if applicable.
#### UI
- [ ] All issue/task requirements work as expected on the UI
- [ ] Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
- [ ] Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px)
- [ ] Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px)
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/ui/CHANGELOG.md), if applicable.
#### API
- [ ] Verify if API specs need to be regenerated.
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
@@ -1,4 +1,17 @@
{
"channel": "${{ env.SLACK_CHANNEL_ID }}",
"text": "${{ env.STATUS_EMOJI }} ${{ env.COMPONENT }} container release ${{ env.RELEASE_TAG }} push ${{ env.STATUS_TEXT }} <${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|View run>"
"attachments": [
{
"color": "${{ env.STATUS_COLOR }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Status:*\n${{ env.STATUS_TEXT }}\n\n${{ env.COMPONENT }} container release ${{ env.RELEASE_TAG }} push ${{ env.STATUS_TEXT }}\n\n<${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|View run>"
}
}
]
}
]
}
@@ -1,4 +1,17 @@
{
"channel": "${{ env.SLACK_CHANNEL_ID }}",
"text": "${{ env.COMPONENT }} container release ${{ env.RELEASE_TAG }} push started... <${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|View run>"
"attachments": [
{
"color": "${{ env.STATUS_COLOR }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Status:*\nStarted\n\n${{ env.COMPONENT }} container release ${{ env.RELEASE_TAG }} push started...\n\n<${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|View run>"
}
}
]
}
]
}
+2 -2
View File
@@ -45,12 +45,12 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/api-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: '/language:${{ matrix.language }}'
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- name: Backport PR
if: steps.label_check.outputs.label_check == 'success'
uses: sorenlouv/backport-github-action@ad888e978060bc1b2798690dd9d03c4036560947 # v9.5.1
uses: sorenlouv/backport-github-action@516854e7c9f962b9939085c9a92ea28411d1ae90 # v10.2.0
with:
github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX }}
+1 -1
View File
@@ -26,6 +26,6 @@ jobs:
steps:
- name: Check PR title format
uses: agenthunt/conventional-commit-checker-action@9e552d650d0e205553ec7792d447929fc78e012b # v2.0.0
uses: agenthunt/conventional-commit-checker-action@f1823f632e95a64547566dcd2c7da920e67117ad # v2.0.1
with:
pr-title-regex: '^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\([^)]+\))?!?: .+'
+1 -1
View File
@@ -28,6 +28,6 @@ jobs:
fetch-depth: 0
- name: Scan for secrets with TruffleHog
uses: trufflesecurity/trufflehog@ad6fc8fb446b8fafbf7ea8193d2d6bfd42f45690 # v3.90.11
uses: trufflesecurity/trufflehog@b84c3d14d189e16da175e2c27fa8136603783ffc # v3.90.12
with:
extra_args: '--results=verified,unknown'
+1 -1
View File
@@ -83,7 +83,7 @@ jobs:
- name: Update PR comment with changelog status
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
body-includes: '<!-- conflict-checker-comment -->'
- name: Create or update comment
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
+1 -1
View File
@@ -382,7 +382,7 @@ jobs:
no-changelog
- name: Create draft release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
tag_name: ${{ env.PROWLER_VERSION }}
name: Prowler ${{ env.PROWLER_VERSION }}
+2 -2
View File
@@ -52,12 +52,12 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/sdk-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: '/language:${{ matrix.language }}'
@@ -39,7 +39,7 @@ jobs:
run: pip install boto3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
+2 -2
View File
@@ -48,12 +48,12 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/ui-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: '/language:${{ matrix.language }}'
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
echo "All database fixtures loaded successfully!"
'
- name: Setup Node.js environment
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: '20.x'
cache: 'npm'
+3 -3
View File
@@ -56,7 +56,7 @@ Prowler includes hundreds of built-in controls to ensure compliance with standar
Prowler App is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
![Prowler App](docs/products/img/overview.png)
![Prowler App](docs/images/products/overview.png)
>For more details, refer to the [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
@@ -73,7 +73,7 @@ prowler <provider>
```console
prowler dashboard
```
![Prowler Dashboard](docs/products/img/dashboard.png)
![Prowler Dashboard](docs/images/products/dashboard.png)
# Prowler at a Glance
> [!Tip]
@@ -90,7 +90,7 @@ prowler dashboard
| M365 | 70 | 7 | 3 | 2 | Official | UI, API, CLI |
| OCI | 51 | 13 | 1 | 10 | Official | UI, API, CLI |
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI |
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | CLI |
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | CLI, API |
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
+6
View File
@@ -15,9 +15,15 @@ All notable changes to the **Prowler API** are documented in this file.
- Support C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
- Support for Amazon Bedrock and OpenAI compatible providers in Lighthouse AI [(#8957)](https://github.com/prowler-cloud/prowler/pull/8957)
- OpenAPI schema integration with Mintlify documentation including compatibility fixes and dynamic server URL configuration
- Support for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
---
## [1.14.2] (Prowler 5.13.2)
### Fixed
- Update unique constraint for `Provider` model to exclude soft-deleted entries, resolving duplicate errors when re-deleting providers.
## [1.14.1] (Prowler 5.13.1)
### Fixed
@@ -0,0 +1,32 @@
# Generated by Django 5.1.13 on 2025-11-05 08:37
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0054_iac_provider"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("mongodbatlas", "MongoDB Atlas"),
("iac", "IaC"),
("oraclecloud", "Oracle Cloud Infrastructure"),
],
default="aws",
),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.13 on 2025-11-06 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0055_mongodbatlas_provider"),
]
operations = [
migrations.RemoveConstraint(
model_name="provider",
name="unique_provider_uids",
),
migrations.AddConstraint(
model_name="provider",
constraint=models.UniqueConstraint(
condition=models.Q(("is_deleted", False)),
fields=("tenant_id", "provider", "uid"),
name="unique_provider_uids",
),
),
]
+12 -1
View File
@@ -284,6 +284,7 @@ class Provider(RowLevelSecurityProtectedModel):
KUBERNETES = "kubernetes", _("Kubernetes")
M365 = "m365", _("M365")
GITHUB = "github", _("GitHub")
MONGODBATLAS = "mongodbatlas", _("MongoDB Atlas")
IAC = "iac", _("IaC")
ORACLECLOUD = "oraclecloud", _("Oracle Cloud Infrastructure")
@@ -381,6 +382,15 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_mongodbatlas_uid(value):
if not re.match(r"^[0-9a-fA-F]{24}$", value):
raise ModelValidationError(
detail="MongoDB Atlas organization ID must be a 24-character hexadecimal string.",
code="mongodbatlas-uid",
pointer="/data/attributes/uid",
)
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
@@ -415,7 +425,8 @@ class Provider(RowLevelSecurityProtectedModel):
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "provider", "uid", "is_deleted"),
fields=("tenant_id", "provider", "uid"),
condition=Q(is_deleted=False),
name="unique_provider_uids",
),
RowLevelSecurityConstraint(
+77 -3
View File
@@ -875,6 +875,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -886,6 +887,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -901,6 +903,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -913,6 +916,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -1410,6 +1414,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -1421,6 +1426,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -1436,6 +1442,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -1448,6 +1455,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -1853,6 +1861,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -1864,6 +1873,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -1879,6 +1889,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -1891,6 +1902,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -2294,6 +2306,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -2305,6 +2318,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -2320,6 +2334,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -2332,6 +2347,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -2723,6 +2739,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -2734,6 +2751,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -2749,6 +2767,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -2761,6 +2780,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -4556,6 +4576,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -4567,6 +4588,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -4582,6 +4604,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -4594,6 +4617,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -4741,6 +4765,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -4752,6 +4777,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -4767,6 +4793,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -4779,6 +4806,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -4992,6 +5020,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -5003,6 +5032,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -5018,6 +5048,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -5030,6 +5061,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -5723,6 +5755,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -5734,6 +5767,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -5749,6 +5783,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -5762,6 +5797,7 @@ paths:
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `mongodbatlas` - MongoDB Atlas
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -6418,6 +6454,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -6429,6 +6466,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -6444,6 +6482,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -6456,6 +6495,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -6791,6 +6831,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -6802,6 +6843,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -6817,6 +6859,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -6829,6 +6872,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -7065,6 +7109,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -7076,6 +7121,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -7091,6 +7137,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -7103,6 +7150,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -7345,6 +7393,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -7356,6 +7405,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -7371,6 +7421,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -7383,6 +7434,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -8188,6 +8240,7 @@ paths:
- azure
- gcp
- github
- mongodbatlas
- iac
- kubernetes
- m365
@@ -8199,6 +8252,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
- in: query
@@ -8214,6 +8268,7 @@ paths:
- gcp
- github
- iac
- mongodbatlas
- kubernetes
- m365
- oraclecloud
@@ -8226,6 +8281,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
explode: false
@@ -14251,7 +14307,7 @@ components:
type: string
description: GitHub OAuth App token for authentication.
required:
- oauth_app_token
- oauth_app_token
- type: object
title: GitHub App Credentials
properties:
@@ -14264,6 +14320,18 @@ components:
required:
- github_app_id
- github_app_key
- type: object
title: MongoDB Atlas API Key
properties:
atlas_public_key:
type: string
description: MongoDB Atlas API public key.
atlas_private_key:
type: string
description: MongoDB Atlas API private key.
required:
- atlas_public_key
- atlas_private_key
- type: object
title: IaC Repository Credentials
properties:
@@ -15290,6 +15358,7 @@ components:
- m365
- github
- iac
- mongodbatlas
- oraclecloud
type: string
description: |-
@@ -15300,6 +15369,7 @@ components:
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `mongodbatlas` - MongoDB Atlas
* `oraclecloud` - Oracle Cloud Infrastructure
x-spec-enum-id: cfe6f885ff538a1a
uid:
@@ -15414,6 +15484,7 @@ components:
- m365
- github
- iac
- mongodbatlas
- oraclecloud
type: string
x-spec-enum-id: cfe6f885ff538a1a
@@ -15427,12 +15498,13 @@ components:
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `mongodbatlas` - MongoDB Atlas
* `oraclecloud` - Oracle Cloud Infrastructure
uid:
type: string
title: Unique identifier for the provider, set by the provider
description: Unique identifier for the provider, set by the provider,
e.g. AWS account ID, Azure subscription ID, GCP project ID, etc.
e.g. AWS account ID, Azure subscription ID, GCP project ID, MongoDB Atlas organization ID, etc.
maxLength: 250
minLength: 3
required:
@@ -15472,6 +15544,7 @@ components:
- m365
- github
- iac
- mongodbatlas
- oraclecloud
type: string
x-spec-enum-id: cfe6f885ff538a1a
@@ -15485,13 +15558,14 @@ components:
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `mongodbatlas` - MongoDB Atlas
* `oraclecloud` - Oracle Cloud Infrastructure
uid:
type: string
minLength: 3
title: Unique identifier for the provider, set by the provider
description: Unique identifier for the provider, set by the provider,
e.g. AWS account ID, Azure subscription ID, GCP project ID, etc.
e.g. AWS account ID, Azure subscription ID, GCP project ID, MongoDB Atlas organization ID, etc.
maxLength: 250
required:
- uid
+8
View File
@@ -20,8 +20,10 @@ from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
@@ -109,6 +111,8 @@ class TestReturnProwlerProvider:
(Provider.ProviderChoices.AZURE.value, AzureProvider),
(Provider.ProviderChoices.KUBERNETES.value, KubernetesProvider),
(Provider.ProviderChoices.M365.value, M365Provider),
(Provider.ProviderChoices.GITHUB.value, GithubProvider),
(Provider.ProviderChoices.MONGODBATLAS.value, MongodbatlasProvider),
(Provider.ProviderChoices.ORACLECLOUD.value, OraclecloudProvider),
],
)
@@ -209,6 +213,10 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.ORACLECLOUD.value,
{},
),
(
Provider.ProviderChoices.MONGODBATLAS.value,
{"atlas_organization_id": "provider_uid"},
),
],
)
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
+194 -7
View File
@@ -1153,6 +1153,11 @@ class TestProviderViewSet:
"uid": "https://gitlab.com/user/project",
"alias": "GitLab Repo",
},
{
"provider": "mongodbatlas",
"uid": "64b1d3c0e4b03b1234567890",
"alias": "Atlas Organization",
},
]
),
)
@@ -1166,6 +1171,161 @@ class TestProviderViewSet:
assert Provider.objects.get().uid == provider_json_payload["uid"]
assert Provider.objects.get().alias == provider_json_payload["alias"]
@pytest.mark.parametrize(
"provider_json_payload",
(
[
{"provider": "aws", "uid": "111111111111", "alias": "test"},
{"provider": "gcp", "uid": "a12322-test54321", "alias": "test"},
{
"provider": "kubernetes",
"uid": "kubernetes-test-123456789",
"alias": "test",
},
{
"provider": "kubernetes",
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
"alias": "EKS",
},
{
"provider": "kubernetes",
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
"alias": "GKE",
},
{
"provider": "kubernetes",
"uid": "gke_project/cluster-name",
"alias": "GKE",
},
{
"provider": "kubernetes",
"uid": "admin@k8s-demo",
"alias": "test",
},
{
"provider": "azure",
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
"alias": "test",
},
{
"provider": "m365",
"uid": "TestingPro.onmicrosoft.com",
"alias": "test",
},
{
"provider": "m365",
"uid": "subdomain.domain.es",
"alias": "test",
},
{
"provider": "m365",
"uid": "microsoft.net",
"alias": "test",
},
{
"provider": "m365",
"uid": "subdomain1.subdomain2.subdomain3.subdomain4.domain.net",
"alias": "test",
},
{
"provider": "github",
"uid": "test-user",
"alias": "test",
},
{
"provider": "github",
"uid": "test-organization",
"alias": "GitHub Org",
},
{
"provider": "github",
"uid": "prowler-cloud",
"alias": "Prowler",
},
{
"provider": "github",
"uid": "microsoft",
"alias": "Microsoft",
},
{
"provider": "github",
"uid": "a12345678901234567890123456789012345678",
"alias": "Long Username",
},
]
),
)
@patch("api.v1.views.Task.objects.get")
@patch("api.v1.views.delete_provider_task.delay")
def test_providers_soft_delete(
self,
mock_delete_task,
mock_task_get,
authenticated_client,
provider_json_payload,
tasks_fixture,
):
# Mock the Celery task response
prowler_task = tasks_fixture[0]
task_mock = Mock()
task_mock.id = prowler_task.id
mock_delete_task.return_value = task_mock
mock_task_get.return_value = prowler_task
# 1.Create a provider
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
provider_id = response.json()["data"]["id"]
# 2. Soft delete the provider using the actual API endpoint
response = authenticated_client.delete(
reverse("provider-detail", kwargs={"pk": provider_id})
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert Provider.objects.count() == 0
assert Provider.all_objects.count() == 1
mock_delete_task.assert_called_once_with(
provider_id=str(provider_id), tenant_id=ANY
)
# 3. Create a provider with the same UID should succeed (since the old one is soft deleted)
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
assert Provider.all_objects.count() == 2
provider_id = response.json()["data"]["id"]
# 4. Creating another provider with the same UID should fail (duplicate)
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_delete_task.reset_mock()
mock_delete_task.return_value = task_mock
# 5. Delete the second provider
response = authenticated_client.delete(
reverse("provider-detail", kwargs={"pk": provider_id})
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert Provider.objects.count() == 0
assert Provider.all_objects.count() == 2
# 6. Creating a provider with the same UID should succeed again
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
assert Provider.all_objects.count() == 3
@pytest.mark.parametrize(
"provider_json_payload, error_code, error_pointer",
(
@@ -1329,6 +1489,24 @@ class TestProviderViewSet:
"iac-uid",
"uid",
),
(
{
"provider": "mongodbatlas",
"uid": "64b1d3c0e4b03b123456789g",
"alias": "test",
},
"mongodbatlas-uid",
"uid",
),
(
{
"provider": "mongodbatlas",
"uid": "1234",
"alias": "test",
},
"mongodbatlas-uid",
"uid",
),
]
),
)
@@ -1502,22 +1680,22 @@ class TestProviderViewSet:
(
"uid.icontains",
"1",
6,
), # Updated: includes OCI provider with "1" in UID
7,
),
("alias", "aws_testing_1", 1),
("alias.icontains", "aws", 2),
("inserted_at", TODAY, 7), # Updated: 7 providers now (added OCI)
("inserted_at", TODAY, 8),
(
"inserted_at.gte",
"2024-01-01",
7,
), # Updated: 7 providers now (added OCI)
8,
),
("inserted_at.lte", "2024-01-01", 0),
(
"updated_at.gte",
"2024-01-01",
7,
), # Updated: 7 providers now (added OCI)
8,
),
("updated_at.lte", "2024-01-01", 0),
]
),
@@ -2057,6 +2235,15 @@ class TestProviderSecretViewSet:
"pass_phrase": "my-secure-passphrase",
},
),
# MongoDB Atlas credentials
(
Provider.ProviderChoices.MONGODBATLAS.value,
ProviderSecret.TypeChoices.STATIC,
{
"atlas_public_key": "public-key",
"atlas_private_key": "private-key",
},
),
],
)
def test_provider_secrets_create_valid(
+13 -3
View File
@@ -21,6 +21,7 @@ from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.iac.iac_provider import IacProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
@@ -70,6 +71,7 @@ def return_prowler_provider(
| IacProvider
| KubernetesProvider
| M365Provider
| MongodbatlasProvider
| OraclecloudProvider
]:
"""Return the Prowler provider class based on the given provider type.
@@ -78,7 +80,7 @@ def return_prowler_provider(
provider (Provider): The provider object containing the provider type and associated secrets.
Returns:
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | OraclecloudProvider: The corresponding provider class.
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | OraclecloudProvider | MongodbatlasProvider: The corresponding provider class.
Raises:
ValueError: If the provider type specified in `provider.provider` is not supported.
@@ -96,6 +98,8 @@ def return_prowler_provider(
prowler_provider = M365Provider
case Provider.ProviderChoices.GITHUB.value:
prowler_provider = GithubProvider
case Provider.ProviderChoices.MONGODBATLAS.value:
prowler_provider = MongodbatlasProvider
case Provider.ProviderChoices.IAC.value:
prowler_provider = IacProvider
case Provider.ProviderChoices.ORACLECLOUD.value:
@@ -146,6 +150,11 @@ def get_prowler_provider_kwargs(
prowler_provider_kwargs["oauth_app_token"] = provider.secret.secret[
"access_token"
]
elif provider.provider == Provider.ProviderChoices.MONGODBATLAS.value:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"atlas_organization_id": provider.uid,
}
if mutelist_processor:
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
@@ -166,6 +175,7 @@ def initialize_prowler_provider(
| IacProvider
| KubernetesProvider
| M365Provider
| MongodbatlasProvider
| OraclecloudProvider
):
"""Initialize a Prowler provider instance based on the given provider type.
@@ -175,8 +185,8 @@ def initialize_prowler_provider(
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | OciProvider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `IacProvider`, `KubernetesProvider`, `M365Provider` or `OraclecloudProvider`) initialized with the
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | OraclecloudProvider | MongodbatlasProvider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `IacProvider`, `KubernetesProvider`, `M365Provider`, `OraclecloudProvider` or `MongodbatlasProvider`) initialized with the
provider's secrets.
"""
prowler_provider = return_prowler_provider(provider)
@@ -289,6 +289,21 @@ from rest_framework_json_api import serializers
},
"required": ["user", "fingerprint", "tenancy", "region"],
},
{
"type": "object",
"title": "MongoDB Atlas API Key",
"properties": {
"atlas_public_key": {
"type": "string",
"description": "MongoDB Atlas API public key.",
},
"atlas_private_key": {
"type": "string",
"description": "MongoDB Atlas API private key.",
},
},
"required": ["atlas_public_key", "atlas_private_key"],
},
]
}
)
+10
View File
@@ -1370,6 +1370,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = M365ProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.ORACLECLOUD.value:
serializer = OracleCloudProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.MONGODBATLAS.value:
serializer = MongoDBAtlasProviderSecret(data=secret)
else:
raise serializers.ValidationError(
{"provider": f"Provider type not supported {provider_type}"}
@@ -1466,6 +1468,14 @@ class GCPServiceAccountProviderSecret(serializers.Serializer):
resource_name = "provider-secrets"
class MongoDBAtlasProviderSecret(serializers.Serializer):
atlas_public_key = serializers.CharField()
atlas_private_key = serializers.CharField()
class Meta:
resource_name = "provider-secrets"
class KubernetesProviderSecret(serializers.Serializer):
kubeconfig_content = serializers.CharField()
+16 -1
View File
@@ -506,8 +506,23 @@ def providers_fixture(tenants_fixture):
alias="oci_testing",
tenant_id=tenant.id,
)
provider8 = Provider.objects.create(
provider="mongodbatlas",
uid="64b1d3c0e4b03b1234567890",
alias="mongodbatlas_testing",
tenant_id=tenant.id,
)
return provider1, provider2, provider3, provider4, provider5, provider6, provider7
return (
provider1,
provider2,
provider3,
provider4,
provider5,
provider6,
provider7,
provider8,
)
@pytest.fixture
+24
View File
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
+2 -2
View File
@@ -33,7 +33,7 @@ The supported providers right now are:
| [Github](/user-guide/providers/github/getting-started-github) | Official | UI, API, CLI |
| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | UI, API, CLI |
| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | UI, API, CLI |
| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | CLI |
| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | CLI, API |
| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | CLI |
| **NHN** | Unofficial | CLI |
@@ -48,4 +48,4 @@ For more information about the checks and compliance of each provider visit [Pro
<Card title="Development Guide" icon="pen-to-square" href="/developer-guide/introduction">
Interested in contributing to Prowler?
</Card>
</Columns>
</Columns>
Generated
+119 -98
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -4420,106 +4420,127 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""}
[[package]]
name = "regex"
version = "2024.11.6"
version = "2025.9.18"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
python-versions = ">=3.9"
groups = ["dev", "docs"]
files = [
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
{file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"},
{file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"},
{file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"},
{file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"},
{file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"},
{file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"},
{file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"},
{file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"},
{file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"},
{file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"},
{file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"},
{file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"},
{file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"},
{file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"},
{file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"},
{file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"},
{file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"},
{file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"},
{file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
{file = "regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788"},
{file = "regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4"},
{file = "regex-2025.9.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61"},
{file = "regex-2025.9.18-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251"},
{file = "regex-2025.9.18-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746"},
{file = "regex-2025.9.18-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2"},
{file = "regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0"},
{file = "regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8"},
{file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea"},
{file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8"},
{file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25"},
{file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29"},
{file = "regex-2025.9.18-cp310-cp310-win32.whl", hash = "sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444"},
{file = "regex-2025.9.18-cp310-cp310-win_amd64.whl", hash = "sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450"},
{file = "regex-2025.9.18-cp310-cp310-win_arm64.whl", hash = "sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442"},
{file = "regex-2025.9.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a"},
{file = "regex-2025.9.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8"},
{file = "regex-2025.9.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414"},
{file = "regex-2025.9.18-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a"},
{file = "regex-2025.9.18-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4"},
{file = "regex-2025.9.18-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a"},
{file = "regex-2025.9.18-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f"},
{file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a"},
{file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9"},
{file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2"},
{file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95"},
{file = "regex-2025.9.18-cp311-cp311-win32.whl", hash = "sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07"},
{file = "regex-2025.9.18-cp311-cp311-win_amd64.whl", hash = "sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9"},
{file = "regex-2025.9.18-cp311-cp311-win_arm64.whl", hash = "sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df"},
{file = "regex-2025.9.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e"},
{file = "regex-2025.9.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a"},
{file = "regex-2025.9.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab"},
{file = "regex-2025.9.18-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5"},
{file = "regex-2025.9.18-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742"},
{file = "regex-2025.9.18-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425"},
{file = "regex-2025.9.18-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352"},
{file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d"},
{file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56"},
{file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e"},
{file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282"},
{file = "regex-2025.9.18-cp312-cp312-win32.whl", hash = "sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459"},
{file = "regex-2025.9.18-cp312-cp312-win_amd64.whl", hash = "sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77"},
{file = "regex-2025.9.18-cp312-cp312-win_arm64.whl", hash = "sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5"},
{file = "regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2"},
{file = "regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb"},
{file = "regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af"},
{file = "regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29"},
{file = "regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f"},
{file = "regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68"},
{file = "regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783"},
{file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac"},
{file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e"},
{file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23"},
{file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f"},
{file = "regex-2025.9.18-cp313-cp313-win32.whl", hash = "sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d"},
{file = "regex-2025.9.18-cp313-cp313-win_amd64.whl", hash = "sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d"},
{file = "regex-2025.9.18-cp313-cp313-win_arm64.whl", hash = "sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb"},
{file = "regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2"},
{file = "regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3"},
{file = "regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12"},
{file = "regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0"},
{file = "regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6"},
{file = "regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef"},
{file = "regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a"},
{file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d"},
{file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368"},
{file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90"},
{file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7"},
{file = "regex-2025.9.18-cp313-cp313t-win32.whl", hash = "sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e"},
{file = "regex-2025.9.18-cp313-cp313t-win_amd64.whl", hash = "sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730"},
{file = "regex-2025.9.18-cp313-cp313t-win_arm64.whl", hash = "sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a"},
{file = "regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129"},
{file = "regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea"},
{file = "regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1"},
{file = "regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47"},
{file = "regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379"},
{file = "regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203"},
{file = "regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164"},
{file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb"},
{file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743"},
{file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282"},
{file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773"},
{file = "regex-2025.9.18-cp314-cp314-win32.whl", hash = "sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788"},
{file = "regex-2025.9.18-cp314-cp314-win_amd64.whl", hash = "sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3"},
{file = "regex-2025.9.18-cp314-cp314-win_arm64.whl", hash = "sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d"},
{file = "regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306"},
{file = "regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946"},
{file = "regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f"},
{file = "regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95"},
{file = "regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b"},
{file = "regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3"},
{file = "regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571"},
{file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad"},
{file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494"},
{file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b"},
{file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41"},
{file = "regex-2025.9.18-cp314-cp314t-win32.whl", hash = "sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096"},
{file = "regex-2025.9.18-cp314-cp314t-win_amd64.whl", hash = "sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a"},
{file = "regex-2025.9.18-cp314-cp314t-win_arm64.whl", hash = "sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01"},
{file = "regex-2025.9.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3dbcfcaa18e9480669030d07371713c10b4f1a41f791ffa5cb1a99f24e777f40"},
{file = "regex-2025.9.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1e85f73ef7095f0380208269055ae20524bfde3f27c5384126ddccf20382a638"},
{file = "regex-2025.9.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9098e29b3ea4ffffeade423f6779665e2a4f8db64e699c0ed737ef0db6ba7b12"},
{file = "regex-2025.9.18-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90b6b7a2d0f45b7ecaaee1aec6b362184d6596ba2092dd583ffba1b78dd0231c"},
{file = "regex-2025.9.18-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c81b892af4a38286101502eae7aec69f7cd749a893d9987a92776954f3943408"},
{file = "regex-2025.9.18-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3b524d010973f2e1929aeb635418d468d869a5f77b52084d9f74c272189c251d"},
{file = "regex-2025.9.18-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b498437c026a3d5d0be0020023ff76d70ae4d77118e92f6f26c9d0423452446"},
{file = "regex-2025.9.18-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0716e4d6e58853d83f6563f3cf25c281ff46cf7107e5f11879e32cb0b59797d9"},
{file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:065b6956749379d41db2625f880b637d4acc14c0a4de0d25d609a62850e96d36"},
{file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d4a691494439287c08ddb9b5793da605ee80299dd31e95fa3f323fac3c33d9d4"},
{file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef8d10cc0989565bcbe45fb4439f044594d5c2b8919d3d229ea2c4238f1d55b0"},
{file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4baeb1b16735ac969a7eeecc216f1f8b7caf60431f38a2671ae601f716a32d25"},
{file = "regex-2025.9.18-cp39-cp39-win32.whl", hash = "sha256:8e5f41ad24a1e0b5dfcf4c4e5d9f5bd54c895feb5708dd0c1d0d35693b24d478"},
{file = "regex-2025.9.18-cp39-cp39-win_amd64.whl", hash = "sha256:50e8290707f2fb8e314ab3831e594da71e062f1d623b05266f8cfe4db4949afd"},
{file = "regex-2025.9.18-cp39-cp39-win_arm64.whl", hash = "sha256:039a9d7195fd88c943d7c777d4941e8ef736731947becce773c31a1009cb3c35"},
{file = "regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4"},
]
[[package]]
+16 -1
View File
@@ -13,7 +13,12 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `cloudstorage_bucket_logging_enabled` check for GCP provider [(#9091)](https://github.com/prowler-cloud/prowler/pull/9091)
- C5 compliance framework for Azure provider [(#9081)](https://github.com/prowler-cloud/prowler/pull/9081)
- C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
- `organization_repository_creation_limited` check for GitHub provider [(#8844)](https://github.com/prowler-cloud/prowler/pull/8844)
- HIPAA compliance framework for the GCP provider [(#8955)](https://github.com/prowler-cloud/prowler/pull/8955)
- Add organization ID parameter for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
- Add multiple compliance improvements [(#9145)](https://github.com/prowler-cloud/prowler/pull/9145)
- Added validation for invalid checks, services, and categories in `load_checks_to_execute` function [(#8971)](https://github.com/prowler-cloud/prowler/pull/8971)
- NIST CSF 2.0 compliance framework for the AWS provider [(#9185)](https://github.com/prowler-cloud/prowler/pull/9185)
### Changed
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
@@ -32,12 +37,22 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS FMS service metadata to new format [(#9005)](https://github.com/prowler-cloud/prowler/pull/9005)
- Update AWS FSx service metadata to new format [(#9006)](https://github.com/prowler-cloud/prowler/pull/9006)
- Update AWS Glacier service metadata to new format [(#9007)](https://github.com/prowler-cloud/prowler/pull/9007)
- Update oraclecloud analytics service metadata to new format [(#9114)](https://github.com/prowler-cloud/prowler/pull/9114)
- Update AWS CodeArtifact service metadata to new format [(#8850)](https://github.com/prowler-cloud/prowler/pull/8850)
- Rename OCI provider to oraclecloud with oci alias [(#9126)](https://github.com/prowler-cloud/prowler/pull/9126)
---
## [v5.13.1] (Prowler UNRELEASED)
## [v5.13.2] (Prowler UNRELEASED)
### Fixed
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
---
## [v5.13.1] (Prowler v5.13.1)
### Fixed
- Add `resource_name` for checks under `logging` for the GCP provider [(#9023)](https://github.com/prowler-cloud/prowler/pull/9023)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"Framework": "ENS",
"Name": "ENS RD 311/2022",
"Name": "ENS RD 311/2022 - Categoría Alta",
"Version": "RD2022",
"Provider": "AWS",
"Description": "The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"Framework": "NIS2",
"Name": "Network and Information Security Directive (Directive (EU) 2022/2555)",
"Name": "NIS2 - Network and Information Security Directive (Directive (EU) 2022/2555)",
"Version": "",
"Provider": "AWS",
"Description": "ANNEX to the Commission Implementing Regulation laying down rules for the application of Directive (EU) 2022/2555 as regards technical and methodological requirements of cybersecurity risk-management measures and further specification of the cases in which an incident is considered to be significant with regard to DNS service providers, TLD name registries, cloud computing service providers, data centre service providers, content delivery network providers, managed service providers, managed security service providers, providers of online market places, of online search engines and of social networking services platforms, and trust service providers",
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
{
"Framework": "ENS",
"Name": "ENS RD 311/2022",
"Name": "ENS RD 311/2022 - Categoría Alta",
"Version": "RD2022",
"Provider": "AZURE",
"Description": "The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"Framework": "NIS2",
"Name": "Network and Information Security Directive (Directive (EU) 2022/2555)",
"Name": "NIS2 - Network and Information Security Directive (Directive (EU) 2022/2555)",
"Version": "",
"Provider": "Azure",
"Description": "ANNEX to the Commission Implementing Regulation laying down rules for the application of Directive (EU) 2022/2555 as regards technical and methodological requirements of cybersecurity risk-management measures and further specification of the cases in which an incident is considered to be significant with regard to DNS service providers, TLD name registries, cloud computing service providers, data centre service providers, content delivery network providers, managed service providers, managed security service providers, providers of online market places, of online search engines and of social networking services platforms, and trust service providers",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"Framework": "ENS",
"Name": "ENS RD 311/2022",
"Name": "ENS RD 311/2022 - Categoría Alta",
"Version": "RD2022",
"Provider": "GCP",
"Description": "The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"Framework": "NIS2",
"Name": "Network and Information Security Directive (Directive (EU) 2022/2555)",
"Name": "NIS2 - Network and Information Security Directive (Directive (EU) 2022/2555)",
"Version": "",
"Provider": "GCP",
"Description": "ANNEX to the Commission Implementing Regulation laying down rules for the application of Directive (EU) 2022/2555 as regards technical and methodological requirements of cybersecurity risk-management measures and further specification of the cases in which an incident is considered to be significant with regard to DNS service providers, TLD name registries, cloud computing service providers, data centre service providers, content delivery network providers, managed service providers, managed security service providers, providers of online market places, of online search engines and of social networking services platforms, and trust service providers",
@@ -476,7 +476,9 @@
{
"Id": "1.2.2",
"Description": "Limit the ability to create repositories to trusted users and teams.",
"Checks": [],
"Checks": [
"organization_repository_creation_limited"
],
"Attributes": [
{
"Section": "1 Source Code",
+65 -1
View File
@@ -1,3 +1,5 @@
import sys
from colorama import Fore, Style
from prowler.lib.check.check import parse_checks_from_file
@@ -57,8 +59,24 @@ def load_checks_to_execute(
# Handle if there are checks passed using -c/--checks
if check_list:
# Validate that all checks exist
available_checks = set(bulk_checks_metadata.keys())
available_checks.update(check_aliases.keys())
invalid_checks = []
for check_name in check_list:
checks_to_execute.add(check_name)
if check_name not in available_checks:
invalid_checks.append(check_name)
else:
checks_to_execute.add(check_name)
if invalid_checks:
logger.critical(
f"Invalid check(s) specified: {', '.join(invalid_checks)}"
)
logger.critical(
f"Please provide valid check names. Use 'prowler {provider} --list-checks' to see available checks."
)
sys.exit(1)
# Handle if there are some severities passed using --severity
elif severities:
@@ -66,6 +84,23 @@ def load_checks_to_execute(
checks_to_execute.update(check_severities[severity])
if service_list:
# Validate that all services exist
available_services = set()
for metadata in bulk_checks_metadata.values():
available_services.add(metadata.ServiceName)
invalid_services = [
s for s in service_list if s not in available_services
]
if invalid_services:
logger.critical(
f"Invalid service(s) specified: {', '.join(invalid_services)}"
)
logger.critical(
f"Please provide valid service names. Use 'prowler {provider} --list-services' to see available services."
)
sys.exit(1)
checks_from_services = set()
for service in service_list:
service_checks = CheckMetadata.list(
@@ -81,6 +116,21 @@ def load_checks_to_execute(
# Handle if there are services passed using -s/--services
elif service_list:
# Validate that all services exist
available_services = set()
for metadata in bulk_checks_metadata.values():
available_services.add(metadata.ServiceName)
invalid_services = [s for s in service_list if s not in available_services]
if invalid_services:
logger.critical(
f"Invalid service(s) specified: {', '.join(invalid_services)}"
)
logger.critical(
f"Please provide valid service names. Use 'prowler {provider} --list-services' to see available services."
)
sys.exit(1)
for service in service_list:
checks_to_execute.update(
CheckMetadata.list(
@@ -103,6 +153,20 @@ def load_checks_to_execute(
# Handle if there are categories passed using --categories
elif categories:
# Validate that all categories exist
available_categories = set(check_categories.keys())
invalid_categories = [
c for c in categories if c not in available_categories
]
if invalid_categories:
logger.critical(
f"Invalid category(ies) specified: {', '.join(invalid_categories)}"
)
logger.critical(
f"Please provide valid category names. Use 'prowler {provider} --list-categories' to see available categories."
)
sys.exit(1)
for category in categories:
checks_to_execute.update(check_categories[category])
+10 -1
View File
@@ -588,8 +588,17 @@ class Check_Report_GCP(Check_Report):
or getattr(resource, "name", None)
or ""
)
# Prefer the explicit resource_name argument, otherwise look for a name attribute on the resource
resource_name_candidate = resource_name or getattr(resource, "name", None)
if not resource_name_candidate and isinstance(resource, dict):
# Some callers pass a dict, so fall back to the dict entry if available
resource_name_candidate = resource.get("name")
if isinstance(resource_name_candidate, str):
# Trim whitespace so empty strings collapse to the default
resource_name_candidate = resource_name_candidate.strip()
self.resource_name = (
resource_name or getattr(resource, "name", "") or "GCP Project"
str(resource_name_candidate) if resource_name_candidate else "GCP Project"
)
self.project_id = project_id or getattr(resource, "project_id", "")
self.location = (
+12 -11
View File
@@ -220,18 +220,19 @@ class PowerShellSession:
if output == "":
return {}
json_match = re.search(r"(\[.*\]|\{.*\})", output, re.DOTALL)
if not json_match:
logger.error(
f"Unexpected PowerShell output: {output}\n",
)
else:
decoder = json.JSONDecoder()
for index, character in enumerate(output):
if character not in ("{", "["):
continue
try:
return json.loads(json_match.group(1))
except json.JSONDecodeError as error:
logger.error(
f"Error parsing PowerShell output as JSON: {str(error)}\n",
)
parsed_json, _ = decoder.raw_decode(output[index:])
return parsed_json
except json.JSONDecodeError:
continue
logger.error(
f"Unexpected PowerShell output: {output}\n",
)
return {}
@@ -0,0 +1,30 @@
{
"Provider": "github",
"CheckID": "organization_repository_creation_limited",
"CheckTitle": "Ensure repository creation is limited to trusted organization members.",
"CheckType": [],
"ServiceName": "organization",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "GitHubOrganization",
"Description": "Ensure that repository creation is restricted so that only trusted owners or specific teams can create new repositories within the organization.",
"Risk": "Allowing all members to create repositories increases the likelihood of shadow repositories, data leakage, or malicious projects being introduced without oversight.",
"RelatedUrl": "https://docs.github.com/en/organizations/managing-organization-settings/restricting-repository-creation-in-your-organization",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable repository creation for members or limit it to specific trusted teams by adjusting Member privileges in the organization's settings.",
"Url": "https://docs.github.com/en/organizations/managing-organization-settings/restricting-repository-creation-in-your-organization"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,106 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGithub
from prowler.providers.github.services.organization.organization_client import (
organization_client,
)
def _join_human_readable(items: List[str]) -> str:
"""Return a simple human readable comma-separated list."""
if not items:
return ""
if len(items) == 1:
return items[0]
return ", ".join(items[:-1]) + f" and {items[-1]}"
class organization_repository_creation_limited(Check):
"""Check if repository creation is limited to trusted organization members."""
def execute(self) -> List[CheckReportGithub]:
findings = []
for org in organization_client.organizations.values():
repo_data = [
getattr(org, "members_can_create_repositories", None),
getattr(org, "members_can_create_public_repositories", None),
getattr(org, "members_can_create_private_repositories", None),
getattr(org, "members_can_create_internal_repositories", None),
getattr(org, "members_allowed_repository_creation_type", None),
]
if all(value is None for value in repo_data):
continue
report = CheckReportGithub(metadata=self.metadata(), resource=org)
global_creation = getattr(org, "members_can_create_repositories", None)
public_creation = getattr(
org, "members_can_create_public_repositories", None
)
private_creation = getattr(
org, "members_can_create_private_repositories", None
)
internal_creation = getattr(
org, "members_can_create_internal_repositories", None
)
creation_type = getattr(
org, "members_allowed_repository_creation_type", None
)
type_flags = []
enabled_types = []
if global_creation is not None:
if global_creation:
enabled_types.append("repositories of any type")
else:
type_flags.append(False)
visibility_flags = [
(public_creation, "public repositories"),
(private_creation, "private repositories"),
(internal_creation, "internal repositories"),
]
for flag, label in visibility_flags:
if flag is not None:
type_flags.append(flag)
if flag:
enabled_types.append(label)
if creation_type:
normalized_type = creation_type.lower()
if normalized_type == "none":
type_flags.append(False)
else:
creation_messages = {
"all": "repositories of any type",
"public": "public repositories",
"private": "private repositories",
"internal": "internal repositories",
"selected": "repositories for selected members or teams",
}
enabled_types.append(
creation_messages.get(
normalized_type, f"{creation_type} repositories"
)
)
restricted = bool(type_flags) and all(flag is False for flag in type_flags)
if restricted:
report.status = "PASS"
report.status_extended = f"Organization {org.name} has disabled repository creation for members."
else:
report.status = "FAIL"
unique_enabled = list(dict.fromkeys(enabled_types))
allowed_desc = _join_human_readable(unique_enabled)
if allowed_desc:
report.status_extended = f"Organization {org.name} allows members to create {allowed_desc}."
else:
report.status_extended = f"Organization {org.name} does not have enough data to confirm repository creation restrictions."
findings.append(report)
return findings
@@ -38,8 +38,9 @@ class Organization(GithubService):
org_names_to_check = set()
try:
for client in getattr(self, "clients", []) or []:
if getattr(self.provider, "organizations", None):
clients = getattr(self, "clients", [])
for client in clients:
if self.provider.organizations:
org_names_to_check.update(self.provider.organizations)
# If repositories are specified without organizations, don't perform organization checks
@@ -147,22 +148,74 @@ class Organization(GithubService):
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
repo_creation_settings = {
"members_can_create_repositories": None,
"members_can_create_public_repositories": None,
"members_can_create_private_repositories": None,
"members_can_create_internal_repositories": None,
"members_allowed_repository_creation_type": None,
}
# Base permission (default repository permission for members)
base_perm: Optional[str] = None
try:
base_perm = getattr(org, "default_repository_permission", None)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
base_perm = None
def _extract_flag(attribute: str, expected_type: type):
"""Return attribute value if it matches expected type and is not a mock placeholder."""
try:
value = getattr(org, attribute, None)
if hasattr(value, "_mock_parent"):
return None
if expected_type is bool and isinstance(value, bool):
return value
if expected_type is str and isinstance(value, str):
return value
return None
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return None
repo_creation_settings["members_can_create_repositories"] = _extract_flag(
"members_can_create_repositories", bool
)
repo_creation_settings["members_can_create_public_repositories"] = (
_extract_flag("members_can_create_public_repositories", bool)
)
repo_creation_settings["members_can_create_private_repositories"] = (
_extract_flag("members_can_create_private_repositories", bool)
)
repo_creation_settings["members_can_create_internal_repositories"] = (
_extract_flag("members_can_create_internal_repositories", bool)
)
repo_creation_settings["members_allowed_repository_creation_type"] = (
_extract_flag("members_allowed_repository_creation_type", str)
)
base_permission_raw = _extract_flag("default_repository_permission", str)
base_permission = (
base_permission_raw.lower()
if isinstance(base_permission_raw, str)
else None
)
organizations[org.id] = Org(
id=org.id,
name=org.login,
mfa_required=require_mfa,
base_permission=base_perm,
members_can_create_repositories=repo_creation_settings[
"members_can_create_repositories"
],
members_can_create_public_repositories=repo_creation_settings[
"members_can_create_public_repositories"
],
members_can_create_private_repositories=repo_creation_settings[
"members_can_create_private_repositories"
],
members_can_create_internal_repositories=repo_creation_settings[
"members_can_create_internal_repositories"
],
members_allowed_repository_creation_type=repo_creation_settings[
"members_allowed_repository_creation_type"
],
base_permission=base_permission,
)
@@ -172,4 +225,9 @@ class Org(BaseModel):
id: int
name: str
mfa_required: Optional[bool] = False
members_can_create_repositories: Optional[bool] = None
members_can_create_public_repositories: Optional[bool] = None
members_can_create_private_repositories: Optional[bool] = None
members_can_create_internal_repositories: Optional[bool] = None
members_allowed_repository_creation_type: Optional[str] = None
base_permission: Optional[str] = None
@@ -1,4 +1,5 @@
import os
from typing import Optional
from prowler.lib.logger import logger
from prowler.lib.powershell.powershell import PowerShellSession
@@ -11,6 +12,7 @@ from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
class M365PowerShell(PowerShellSession):
CONNECT_TIMEOUT = 15
"""
Microsoft 365 specific PowerShell session management implementation.
@@ -123,6 +125,23 @@ class M365PowerShell(PowerShellSession):
'$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token'
)
def _execute_connect_command(
self, command: str, timeout: Optional[int] = None
) -> str:
"""
Execute a PowerShell connect command ensuring empty responses surface as timeouts.
Args:
command (str): PowerShell connect command to run.
timeout (Optional[int]): Timeout in seconds for the command execution.
Returns:
str: Command output or 'Timeout' if the command produced no output.
"""
effective_timeout = timeout or self.CONNECT_TIMEOUT
result = self.execute(command, timeout=effective_timeout)
return result or "Timeout"
def test_credentials(self, credentials: M365Credentials) -> bool:
"""
Test Microsoft 365 credentials by attempting to authenticate against Entra ID.
@@ -141,24 +160,32 @@ class M365PowerShell(PowerShellSession):
# Test Certificate Auth
if credentials.certificate_content and credentials.client_id:
try:
self.test_teams_certificate_connection() or self.test_exchange_certificate_connection()
logger.info("Testing Microsoft Graph Certificate connection...")
self.test_graph_certificate_connection()
logger.info("Microsoft Graph Certificate connection successful")
teams_connection_successful = self.test_teams_certificate_connection()
if not teams_connection_successful:
self.test_exchange_certificate_connection()
return True
except Exception as e:
logger.error(f"Exchange Online Certificate connection failed: {e}")
else:
# Test Microsoft Graph connection
try:
logger.info("Testing Microsoft Graph connection...")
self.test_graph_connection()
logger.info("Microsoft Graph connection successful")
return True
except Exception as e:
logger.error(f"Microsoft Graph connection failed: {e}")
logger.error(f"Microsoft Graph Cer connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message="Check your Microsoft Application credentials and ensure the app has proper permissions",
message="Check your Microsoft Application Certificate and ensure the app has proper permissions",
)
else:
try:
logger.info("Testing Microsoft Graph Client Secret connection...")
self.test_graph_connection()
logger.info("Microsoft Graph Client Secret connection successful")
return True
except Exception as e:
logger.error(f"Microsoft Graph Client Secret connection failed: {e}")
raise M365GraphConnectionError(
file=os.path.basename(__file__),
original_exception=e,
message="Check your Microsoft Application Client Secret and ensure the app has proper permissions",
)
def test_graph_connection(self) -> bool:
@@ -178,6 +205,16 @@ class M365PowerShell(PowerShellSession):
message=f"Failed to connect to Microsoft Graph API: {str(e)}",
)
def test_graph_certificate_connection(self) -> bool:
"""Test Microsoft Graph API connection using certificate and raise exception if it fails."""
result = self._execute_connect_command(
"Connect-Graph -Certificate $certificate -AppId $clientID -TenantId $tenantID"
)
if "Welcome to Microsoft Graph!" not in result:
logger.error(f"Microsoft Graph Certificate connection failed: {result}")
return False
return True
def test_teams_connection(self) -> bool:
"""Test Microsoft Teams API connection and raise exception if it fails."""
try:
@@ -195,7 +232,7 @@ class M365PowerShell(PowerShellSession):
"Microsoft Teams connection failed: Please check your permissions and try again."
)
return False
self.execute(
self._execute_connect_command(
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
)
return True
@@ -207,7 +244,7 @@ class M365PowerShell(PowerShellSession):
def test_teams_certificate_connection(self) -> bool:
"""Test Microsoft Teams API connection using certificate and raise exception if it fails."""
result = self.execute(
result = self._execute_connect_command(
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
)
if self.tenant_identity.identity_id not in result:
@@ -231,8 +268,9 @@ class M365PowerShell(PowerShellSession):
"Exchange Online connection failed: Please check your permissions and try again."
)
return False
self.execute(
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
self._execute_connect_command(
'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"',
timeout=self.CONNECT_TIMEOUT,
)
return True
except Exception as e:
@@ -243,8 +281,9 @@ class M365PowerShell(PowerShellSession):
def test_exchange_certificate_connection(self) -> bool:
"""Test Exchange Online API connection using certificate and raise exception if it fails."""
result = self.execute(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
result = self._execute_connect_command(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
timeout=self.CONNECT_TIMEOUT,
)
if "https://aka.ms/exov3-module" not in result:
logger.error(f"Exchange Online Certificate connection failed: {result}")
@@ -290,7 +329,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTeamsClientConfiguration | ConvertTo-Json", json_parse=True
"Get-CsTeamsClientConfiguration | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_global_meeting_policy(self) -> dict:
@@ -309,7 +349,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTeamsMeetingPolicy -Identity Global | ConvertTo-Json",
"Get-CsTeamsMeetingPolicy -Identity Global | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -329,7 +369,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTeamsMessagingPolicy -Identity Global | ConvertTo-Json",
"Get-CsTeamsMessagingPolicy -Identity Global | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -349,7 +389,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-CsTenantFederationConfiguration | ConvertTo-Json", json_parse=True
"Get-CsTenantFederationConfiguration | ConvertTo-Json -Depth 10",
json_parse=True,
)
def connect_exchange_online(self) -> dict:
@@ -389,7 +430,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled | ConvertTo-Json",
"Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -409,7 +450,9 @@ class M365PowerShell(PowerShellSession):
"Identity": "Default"
}
"""
return self.execute("Get-MalwareFilterPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-MalwareFilterPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_malware_filter_rule(self) -> dict:
"""
@@ -427,7 +470,9 @@ class M365PowerShell(PowerShellSession):
"State": "Enabled"
}
"""
return self.execute("Get-MalwareFilterRule | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-MalwareFilterRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_outbound_spam_filter_policy(self) -> dict:
"""
@@ -448,7 +493,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedOutboundSpamFilterPolicy | ConvertTo-Json", json_parse=True
"Get-HostedOutboundSpamFilterPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_outbound_spam_filter_rule(self) -> dict:
@@ -467,7 +513,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedOutboundSpamFilterRule | ConvertTo-Json", json_parse=True
"Get-HostedOutboundSpamFilterRule | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_antiphishing_policy(self) -> dict:
@@ -493,7 +540,9 @@ class M365PowerShell(PowerShellSession):
"IsDefault": false
}
"""
return self.execute("Get-AntiPhishPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-AntiPhishPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_antiphishing_rules(self) -> dict:
"""
@@ -511,7 +560,9 @@ class M365PowerShell(PowerShellSession):
"State": Enabled,
}
"""
return self.execute("Get-AntiPhishRule | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-AntiPhishRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_organization_config(self) -> dict:
"""
@@ -530,7 +581,9 @@ class M365PowerShell(PowerShellSession):
"AuditDisabled": false
}
"""
return self.execute("Get-OrganizationConfig | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-OrganizationConfig | ConvertTo-Json -Depth 10", json_parse=True
)
def get_mailbox_audit_config(self) -> dict:
"""
@@ -550,7 +603,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-MailboxAuditBypassAssociation | ConvertTo-Json", json_parse=True
"Get-MailboxAuditBypassAssociation | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_mailbox_policy(self) -> dict:
@@ -569,7 +623,9 @@ class M365PowerShell(PowerShellSession):
"AdditionalStorageProvidersAvailable": True
}
"""
return self.execute("Get-OwaMailboxPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-OwaMailboxPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_external_mail_config(self) -> dict:
"""
@@ -587,7 +643,9 @@ class M365PowerShell(PowerShellSession):
"ExternalMailTagEnabled": true
}
"""
return self.execute("Get-ExternalInOutlook | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-ExternalInOutlook | ConvertTo-Json -Depth 10", json_parse=True
)
def get_transport_rules(self) -> dict:
"""
@@ -606,7 +664,9 @@ class M365PowerShell(PowerShellSession):
"SenderDomainIs": ["example.com"]
}
"""
return self.execute("Get-TransportRule | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-TransportRule | ConvertTo-Json -Depth 10", json_parse=True
)
def get_connection_filter_policy(self) -> dict:
"""
@@ -625,7 +685,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedConnectionFilterPolicy -Identity Default | ConvertTo-Json",
"Get-HostedConnectionFilterPolicy -Identity Default | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -645,7 +705,9 @@ class M365PowerShell(PowerShellSession):
"Enabled": true
}
"""
return self.execute("Get-DkimSigningConfig | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-DkimSigningConfig | ConvertTo-Json -Depth 10", json_parse=True
)
def get_inbound_spam_filter_policy(self) -> dict:
"""
@@ -664,7 +726,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True
"Get-HostedContentFilterPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_inbound_spam_filter_rule(self) -> dict:
@@ -684,7 +747,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-HostedContentFilterRule | ConvertTo-Json", json_parse=True
"Get-HostedContentFilterRule | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_report_submission_policy(self) -> dict:
@@ -715,7 +779,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-ReportSubmissionPolicy | ConvertTo-Json", json_parse=True
"Get-ReportSubmissionPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_role_assignment_policies(self) -> dict:
@@ -736,7 +801,8 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-RoleAssignmentPolicy | ConvertTo-Json", json_parse=True
"Get-RoleAssignmentPolicy | ConvertTo-Json -Depth 10",
json_parse=True,
)
def get_mailbox_audit_properties(self) -> dict:
@@ -801,7 +867,7 @@ class M365PowerShell(PowerShellSession):
}
"""
return self.execute(
"Get-EXOMailbox -PropertySets Audit -ResultSize Unlimited | ConvertTo-Json",
"Get-EXOMailbox -PropertySets Audit -ResultSize Unlimited | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -820,7 +886,9 @@ class M365PowerShell(PowerShellSession):
"SmtpClientAuthenticationDisabled": True,
}
"""
return self.execute("Get-TransportConfig | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-TransportConfig | ConvertTo-Json -Depth 10", json_parse=True
)
def get_sharing_policy(self) -> dict:
"""
@@ -838,7 +906,9 @@ class M365PowerShell(PowerShellSession):
"Enabled": true
}
"""
return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True)
return self.execute(
"Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True
)
def get_user_account_status(self) -> dict:
"""
@@ -850,7 +920,7 @@ class M365PowerShell(PowerShellSession):
dict: User account status settings in JSON format.
"""
return self.execute(
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json",
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -54,6 +54,7 @@ class MongodbatlasProvider(Provider):
mutelist_content: dict = None,
# Optional filters
atlas_project_id: str = None,
atlas_organization_id: str = None,
):
"""
MongoDB Atlas Provider constructor
@@ -67,6 +68,7 @@ class MongodbatlasProvider(Provider):
mutelist_path: Path to the mutelist file
mutelist_content: Mutelist content
atlas_project_id: Project ID to filter
atlas_organization_id: Organization ID
"""
logger.info("Instantiating MongoDB Atlas Provider...")
@@ -79,6 +81,7 @@ class MongodbatlasProvider(Provider):
# Store filter options
self._project_id = atlas_project_id
self._organization_id = atlas_organization_id
# Audit Config
if config_content:
@@ -292,6 +295,7 @@ class MongodbatlasProvider(Provider):
atlas_public_key: str = "",
atlas_private_key: str = "",
raise_on_exception: bool = True,
provider_id: str = None,
) -> Connection:
"""
Test connection to MongoDB Atlas
@@ -300,7 +304,7 @@ class MongodbatlasProvider(Provider):
atlas_public_key: MongoDB Atlas API public key
atlas_private_key: MongoDB Atlas API private key
raise_on_exception: Whether to raise exceptions
provider_id: MongoDB Atlas project ID to validate access (added for API compatibility)
Returns:
Connection: Connection status
"""
@@ -1,34 +1,38 @@
{
"Provider": "oraclecloud",
"CheckID": "analytics_instance_access_restricted",
"CheckTitle": "Ensure Oracle Analytics Cloud (OAC) access is restricted to allowed sources or deployed within a Virtual Cloud Network",
"CheckType": [
"Software and Configuration Checks",
"Industry and Regulatory Standards",
"CIS OCI Foundations Benchmark"
],
"CheckTitle": "Oracle Analytics Cloud instance is deployed within a Virtual Cloud Network or restricts public access to allowed sources",
"CheckType": [],
"ServiceName": "analytics",
"SubServiceName": "",
"ResourceIdTemplate": "oci:analytics:instance",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AnalyticsInstance",
"Description": "Oracle Analytics Cloud access should be restricted or deployed in VCN.",
"Risk": "Not meeting this network security requirement increases risk of unauthorized access.",
"RelatedUrl": "https://docs.oracle.com/en-us/iaas/Content/Network/home.htm",
"Description": "Oracle Analytics Cloud endpoints are evaluated for **network exposure**. Public endpoints must use **restricted allowlists** of specific IPs/CIDRs; presence of `0.0.0.0/0` or no allowed sources indicates unrestricted access. Instances using a **VCN/private endpoint** or public endpoints limited to specific sources align with the intended exposure model.",
"Risk": "Unrestricted OAC endpoints allow Internet-wide access to the login surface, enabling **credential stuffing** and **brute force**. Account takeover can expose **reports and data sources** (**confidentiality**), permit **dashboard/model changes** (**integrity**), and support **lateral movement** into connected systems.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.public.content.oci.oraclecloud.com/en-us/iaas/analytics-cloud/doc/public-endpoints-and-access-control-rules.html",
"https://docs.oracle.com/en/cloud/paas/analytics-cloud/acsds/connect-databases-deployed-public-ip-address.html",
"https://docs.oracle.com/en/cloud/paas/analytics-cloud/acoci/top-faqs-public-or-private-endpoint-security.html",
"https://docs.oracle.com/en/cloud/paas/analytics-cloud/acoci/manage-ingress-access-rules-public-endpoint-using-console.html",
"https://docs.oracle.com/en-us/iaas/analytics-cloud/doc/public-endpoints-and-access-control-rules.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. In OCI Console, go to Analytics & AI > Analytics Cloud and select your instance\n2. On Instance Details, under Network Access, click Edit next to Access Control\n3. Remove any 0.0.0.0/0 entry (if present)\n4. Add an access rule with the specific allowed public IP or CIDR\n5. Click Save",
"Terraform": "```hcl\nresource \"oci_analytics_analytics_instance\" \"example\" {\n compartment_id = \"<example_resource_id>\"\n name = \"<example_resource_name>\"\n feature_set = \"ENTERPRISE_ANALYTICS\"\n license_type = \"LICENSE_INCLUDED\"\n idcs_access_token = \"<example_resource_id>\"\n\n capacity {\n capacity_type = \"OLPU_COUNT\"\n capacity_value = 1\n }\n\n network_endpoint_details {\n network_endpoint_type = \"PUBLIC\"\n whitelisted_ips = [\"<example_resource_id>\"] # Critical: restrict to specific allowed CIDR; not 0.0.0.0/0\n }\n}\n```"
},
"Recommendation": {
"Text": "Ensure Oracle Analytics Cloud (OAC) access is restricted to allowed sources or deployed within a Virtual Cloud Network",
"Url": "https://hub.prowler.com/check/oci/analytics_instance_access_restricted"
"Text": "Prefer **private deployment in a VCN** and apply **least privilege** network access. *If public is required*, enforce **allowlists** to specific IPs/CIDRs and never include `0.0.0.0/0`. Use **private access channels/service gateways**, require **MFA/SSO**, and apply **defense in depth** (WAF, audit monitoring) to reduce exposure.",
"Url": "https://hub.prowler.com/check/analytics_instance_access_restricted"
}
},
"Categories": [
"network-security"
"internet-exposed",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
+148 -6
View File
@@ -1,3 +1,4 @@
import pytest
from mock import patch
from prowler.lib.check.checks_loader import (
@@ -190,18 +191,22 @@ class TestCheckLoader:
def test_load_checks_to_execute_with_severities_and_services_not_within_severity(
self,
):
"""Test that service not in metadata causes sys.exit(1) when used with severities"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
service_list = ["ec2"]
severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY]
assert set() == load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
service_list=service_list,
severities=severities,
provider=self.provider,
)
# ec2 service doesn't exist in the metadata, so it should exit with error
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
service_list=service_list,
severities=severities,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_checks_file(
self,
@@ -382,3 +387,140 @@ class TestCheckLoader:
categories=categories,
provider=self.provider,
)
def test_load_checks_to_execute_with_invalid_check(self):
"""Test that invalid check names cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
check_list = ["invalid_check_name"]
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
check_list=check_list,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_multiple_invalid_checks(self):
"""Test that multiple invalid check names cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
check_list = ["invalid_check_1", "invalid_check_2", "invalid_check_3"]
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
check_list=check_list,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_mixed_valid_invalid_checks(self):
"""Test that mix of valid and invalid checks cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
check_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME, "invalid_check"]
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
check_list=check_list,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_invalid_service(self):
"""Test that invalid service names cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
service_list = ["invalid_service"]
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
service_list=service_list,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_invalid_service_and_severity(self):
"""Test that invalid service names with severity cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
service_list = ["invalid_service"]
severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY]
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
service_list=service_list,
severities=severities,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_multiple_invalid_services(self):
"""Test that multiple invalid service names cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
service_list = ["invalid_service_1", "invalid_service_2"]
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
service_list=service_list,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_invalid_category(self):
"""Test that invalid category names cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
categories = {"invalid_category"}
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
categories=categories,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_multiple_invalid_categories(self):
"""Test that multiple invalid category names cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
categories = {"invalid_category_1", "invalid_category_2"}
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
categories=categories,
provider=self.provider,
)
assert exc_info.value.code == 1
def test_load_checks_to_execute_with_mixed_valid_invalid_categories(self):
"""Test that mix of valid and invalid categories cause sys.exit(1)"""
bulk_checks_metatada = {
S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata()
}
categories = {"internet-exposed", "invalid_category"}
with pytest.raises(SystemExit) as exc_info:
load_checks_to_execute(
bulk_checks_metadata=bulk_checks_metatada,
categories=categories,
provider=self.provider,
)
assert exc_info.value.code == 1
@@ -135,5 +135,5 @@ class TestAWSENS:
mock_file.seek(0)
content = mock_file.read()
expected_csv = f"PROVIDER;DESCRIPTION;ACCOUNTID;REGION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL;REQUIREMENTS_ATTRIBUTES_MARCO;REQUIREMENTS_ATTRIBUTES_CATEGORIA;REQUIREMENTS_ATTRIBUTES_DESCRIPCIONCONTROL;REQUIREMENTS_ATTRIBUTES_NIVEL;REQUIREMENTS_ATTRIBUTES_TIPO;REQUIREMENTS_ATTRIBUTES_DIMENSIONES;REQUIREMENTS_ATTRIBUTES_MODOEJECUCION;REQUIREMENTS_ATTRIBUTES_DEPENDENCIAS;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME\r\naws;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;123456789012;eu-west-1;{datetime.now()};op.exp.8.aws.ct.3;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;PASS;;;service_test_check_id;False;;ENS;ENS RD 311/2022\r\naws;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;;;{datetime.now()};op.exp.8.aws.ct.4;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;MANUAL;Manual check;manual_check;manual;False;Manual check;ENS;ENS RD 311/2022\r\n"
expected_csv = f"PROVIDER;DESCRIPTION;ACCOUNTID;REGION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL;REQUIREMENTS_ATTRIBUTES_MARCO;REQUIREMENTS_ATTRIBUTES_CATEGORIA;REQUIREMENTS_ATTRIBUTES_DESCRIPCIONCONTROL;REQUIREMENTS_ATTRIBUTES_NIVEL;REQUIREMENTS_ATTRIBUTES_TIPO;REQUIREMENTS_ATTRIBUTES_DIMENSIONES;REQUIREMENTS_ATTRIBUTES_MODOEJECUCION;REQUIREMENTS_ATTRIBUTES_DEPENDENCIAS;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME\r\naws;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;123456789012;eu-west-1;{datetime.now()};op.exp.8.aws.ct.3;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;PASS;;;service_test_check_id;False;;ENS;ENS RD 311/2022 - Categoría Alta\r\naws;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;;;{datetime.now()};op.exp.8.aws.ct.4;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;MANUAL;Manual check;manual_check;manual;False;Manual check;ENS;ENS RD 311/2022 - Categoría Alta\r\n"
assert content == expected_csv
@@ -144,6 +144,6 @@ class TestAzureENS:
mock_file.seek(0)
content = mock_file.read()
expected_csv = f"PROVIDER;DESCRIPTION;SUBSCRIPTIONID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL;REQUIREMENTS_ATTRIBUTES_MARCO;REQUIREMENTS_ATTRIBUTES_CATEGORIA;REQUIREMENTS_ATTRIBUTES_DESCRIPCIONCONTROL;REQUIREMENTS_ATTRIBUTES_NIVEL;REQUIREMENTS_ATTRIBUTES_TIPO;REQUIREMENTS_ATTRIBUTES_DIMENSIONES;REQUIREMENTS_ATTRIBUTES_MODOEJECUCION;REQUIREMENTS_ATTRIBUTES_DEPENDENCIAS;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME\r\nazure;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;123456789012;global;{datetime.now()};op.exp.8.azure.ct.3;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;PASS;;;service_test_check_id;False;;ENS;ENS RD 311/2022\r\nazure;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;;;{datetime.now()};op.exp.8.azure.ct.4;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;MANUAL;Manual check;manual_check;manual;False;Manual check;ENS;ENS RD 311/2022\r\n"
expected_csv = f"PROVIDER;DESCRIPTION;SUBSCRIPTIONID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL;REQUIREMENTS_ATTRIBUTES_MARCO;REQUIREMENTS_ATTRIBUTES_CATEGORIA;REQUIREMENTS_ATTRIBUTES_DESCRIPCIONCONTROL;REQUIREMENTS_ATTRIBUTES_NIVEL;REQUIREMENTS_ATTRIBUTES_TIPO;REQUIREMENTS_ATTRIBUTES_DIMENSIONES;REQUIREMENTS_ATTRIBUTES_MODOEJECUCION;REQUIREMENTS_ATTRIBUTES_DEPENDENCIAS;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME\r\nazure;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;123456789012;global;{datetime.now()};op.exp.8.azure.ct.3;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;PASS;;;service_test_check_id;False;;ENS;ENS RD 311/2022 - Categoría Alta\r\nazure;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;;;{datetime.now()};op.exp.8.azure.ct.4;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;MANUAL;Manual check;manual_check;manual;False;Manual check;ENS;ENS RD 311/2022 - Categoría Alta\r\n"
assert content == expected_csv
@@ -144,6 +144,6 @@ class TestGCPENS:
mock_file.seek(0)
content = mock_file.read()
expected_csv = f"PROVIDER;DESCRIPTION;PROJECTID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL;REQUIREMENTS_ATTRIBUTES_MARCO;REQUIREMENTS_ATTRIBUTES_CATEGORIA;REQUIREMENTS_ATTRIBUTES_DESCRIPCIONCONTROL;REQUIREMENTS_ATTRIBUTES_NIVEL;REQUIREMENTS_ATTRIBUTES_TIPO;REQUIREMENTS_ATTRIBUTES_DIMENSIONES;REQUIREMENTS_ATTRIBUTES_MODOEJECUCION;REQUIREMENTS_ATTRIBUTES_DEPENDENCIAS;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME\r\ngcp;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;123456789012;global;{datetime.now()};op.exp.8.gcp.ct.3;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;PASS;;;service_test_check_id;False;;ENS;ENS RD 311/2022\r\ngcp;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;;;{datetime.now()};op.exp.8.gcp.ct.4;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;MANUAL;Manual check;manual_check;manual;False;Manual check;ENS;ENS RD 311/2022\r\n"
expected_csv = f"PROVIDER;DESCRIPTION;PROJECTID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL;REQUIREMENTS_ATTRIBUTES_MARCO;REQUIREMENTS_ATTRIBUTES_CATEGORIA;REQUIREMENTS_ATTRIBUTES_DESCRIPCIONCONTROL;REQUIREMENTS_ATTRIBUTES_NIVEL;REQUIREMENTS_ATTRIBUTES_TIPO;REQUIREMENTS_ATTRIBUTES_DIMENSIONES;REQUIREMENTS_ATTRIBUTES_MODOEJECUCION;REQUIREMENTS_ATTRIBUTES_DEPENDENCIAS;STATUS;STATUSEXTENDED;RESOURCEID;CHECKID;MUTED;RESOURCENAME;FRAMEWORK;NAME\r\ngcp;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;123456789012;global;{datetime.now()};op.exp.8.gcp.ct.3;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;PASS;;;service_test_check_id;False;;ENS;ENS RD 311/2022 - Categoría Alta\r\ngcp;The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.;;;{datetime.now()};op.exp.8.gcp.ct.4;Registro de actividad;op.exp.8;operacional;explotación;Habilitar la validación de archivos en todos los trails, evitando así que estos se vean modificados o eliminados.;alto;requisito;trazabilidad;automático;;MANUAL;Manual check;manual_check;manual;False;Manual check;ENS;ENS RD 311/2022 - Categoría Alta\r\n"
assert content == expected_csv
+3 -3
View File
@@ -490,7 +490,7 @@ MITRE_ATTACK_GCP = Compliance(
ENS_RD2022_AWS = Compliance(
Framework="ENS",
Name="ENS RD 311/2022",
Name="ENS RD 311/2022 - Categoría Alta",
Provider="AWS",
Version="RD2022",
Description="The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.",
@@ -538,7 +538,7 @@ ENS_RD2022_AWS = Compliance(
ENS_RD2022_AZURE = Compliance(
Framework="ENS",
Name="ENS RD 311/2022",
Name="ENS RD 311/2022 - Categoría Alta",
Provider="Azure",
Version="RD2022",
Description="The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.",
@@ -585,7 +585,7 @@ ENS_RD2022_AZURE = Compliance(
)
ENS_RD2022_GCP = Compliance(
Framework="ENS",
Name="ENS RD 311/2022",
Name="ENS RD 311/2022 - Categoría Alta",
Provider="GCP",
Version="RD2022",
Description="The accreditation scheme of the ENS (National Security Scheme) has been developed by the Ministry of Finance and Public Administrations and the CCN (National Cryptological Center). This includes the basic principles and minimum requirements necessary for the adequate protection of information.",
+8
View File
@@ -216,6 +216,14 @@ class TestPowerShellSession:
result = session.json_parse_output('prefix [{"key": "value"}] suffix')
assert result == [{"key": "value"}]
result = session.json_parse_output(
'INFO {context data} {"key": "value", "list": [1, 2]} extra'
)
assert result == {"key": "value", "list": [1, 2]}
result = session.json_parse_output('{"key": "value"} trailing {log}')
assert result == {"key": "value"}
# Test non-JSON text returns empty dict
result = session.json_parse_output("just some text")
assert result == {}
@@ -0,0 +1,170 @@
from unittest import mock
from prowler.providers.github.services.organization.organization_service import Org
from tests.providers.github.github_fixtures import set_mocked_github_provider
class Test_organization_repository_creation_limited:
def test_no_organizations(self):
organization_client = mock.MagicMock
organization_client.organizations = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited import (
organization_repository_creation_limited,
)
check = organization_repository_creation_limited()
result = check.execute()
assert len(result) == 0
def test_repository_creation_disabled_globally(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
mfa_required=None,
members_can_create_repositories=False,
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited import (
organization_repository_creation_limited,
)
check = organization_repository_creation_limited()
result = check.execute()
assert len(result) == 1
assert result[0].resource_name == org_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Organization {org_name} has disabled repository creation for members."
)
def test_repository_creation_allowed_for_members(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
mfa_required=None,
members_can_create_repositories=True,
members_can_create_public_repositories=True,
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited import (
organization_repository_creation_limited,
)
check = organization_repository_creation_limited()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "public repositories" in result[0].status_extended
assert "repositories of any type" in result[0].status_extended
def test_repository_creation_disabled_per_visibility(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
mfa_required=None,
members_can_create_public_repositories=False,
members_can_create_private_repositories=False,
members_can_create_internal_repositories=False,
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited import (
organization_repository_creation_limited,
)
check = organization_repository_creation_limited()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Organization {org_name} has disabled repository creation for members."
)
def test_repository_creation_disabled_via_members_allowed_type(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
mfa_required=None,
members_allowed_repository_creation_type="none",
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_repository_creation_limited.organization_repository_creation_limited import (
organization_repository_creation_limited,
)
check = organization_repository_creation_limited()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Organization {org_name} has disabled repository creation for members."
)
+20
View File
@@ -17,6 +17,10 @@ SAMPLE_TRIVY_OUTPUT = {
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0001",
"RuleID": "AVD-AWS-0001",
"CauseMetadata": {
"StartLine": 10,
"EndLine": 15,
},
},
{
"ID": "AVD-AWS-0002",
@@ -27,6 +31,10 @@ SAMPLE_TRIVY_OUTPUT = {
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0002",
"RuleID": "AVD-AWS-0002",
"CauseMetadata": {
"StartLine": 20,
"EndLine": 25,
},
},
],
"Vulnerabilities": [],
@@ -46,6 +54,10 @@ SAMPLE_TRIVY_OUTPUT = {
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0003",
"RuleID": "AVD-AWS-0003",
"CauseMetadata": {
"StartLine": 30,
"EndLine": 35,
},
}
],
"Vulnerabilities": [],
@@ -67,6 +79,10 @@ SAMPLE_FAILED_CHECK = {
"Severity": "low",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0001",
"RuleID": "AVD-AWS-0001",
"CauseMetadata": {
"StartLine": 10,
"EndLine": 15,
},
}
SAMPLE_PASSED_CHECK = {
@@ -78,6 +94,10 @@ SAMPLE_PASSED_CHECK = {
"Severity": "low",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0003",
"RuleID": "AVD-AWS-0003",
"CauseMetadata": {
"StartLine": 30,
"EndLine": 35,
},
}
# Additional sample checks
@@ -936,7 +936,8 @@ class Testm365PowerShell:
assert result is True
session.execute.assert_called_once_with(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
timeout=M365PowerShell.CONNECT_TIMEOUT,
)
session.close()
@@ -960,7 +961,8 @@ class Testm365PowerShell:
assert result is False
session.execute.assert_called_once_with(
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
"Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
timeout=M365PowerShell.CONNECT_TIMEOUT,
)
session.close()
@@ -979,7 +981,7 @@ class Testm365PowerShell:
session = M365PowerShell(credentials, identity)
# Mock successful Teams connection - the method returns bool
def mock_execute_side_effect(command):
def mock_execute_side_effect(command, *_, **__):
if "Connect-MicrosoftTeams" in command:
# Return result that contains the identity_id for success
return "Connected successfully test_identity_id"
@@ -991,7 +993,8 @@ class Testm365PowerShell:
assert result is True
session.execute.assert_called_once_with(
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID",
timeout=M365PowerShell.CONNECT_TIMEOUT,
)
session.close()
@@ -1007,7 +1010,7 @@ class Testm365PowerShell:
session = M365PowerShell(credentials, identity)
# Mock failed Teams connection
def mock_execute_side_effect(command, json_parse=False):
def mock_execute_side_effect(command, **kwargs):
if "Connect-MicrosoftTeams" in command:
raise Exception("Connection failed: Authentication error")
return ""
@@ -1090,7 +1093,7 @@ class Testm365PowerShell:
# Mock certificate variable check and teams connection
execute_calls = []
def mock_execute_side_effect(command, json_parse=False):
def mock_execute_side_effect(command):
execute_calls.append(command)
if "Write-Output $certificate" in command:
return "certificate_content" # Non-empty means certificate exists
@@ -1123,7 +1126,7 @@ class Testm365PowerShell:
# Mock certificate variable check and exchange connection
execute_calls = []
def mock_execute_side_effect(command, json_parse=False):
def mock_execute_side_effect(command):
execute_calls.append(command)
if "Write-Output $certificate" in command:
return "certificate_content" # Non-empty means certificate exists
Regular → Executable
+134 -1
View File
@@ -1 +1,134 @@
npm run healthcheck
#!/bin/bash
# Prowler UI - Pre-Commit Hook
# Optionally validates ONLY staged files against AGENTS.md standards using Claude Code
# Controlled by CODE_REVIEW_ENABLED in .env
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚀 Prowler UI - Pre-Commit Hook"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Load .env file (look in git root directory)
GIT_ROOT=$(git rev-parse --show-toplevel)
if [ -f "$GIT_ROOT/ui/.env" ]; then
CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" "$GIT_ROOT/ui/.env" | cut -d'=' -f2 | tr -d ' ')
elif [ -f "$GIT_ROOT/.env" ]; then
CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" "$GIT_ROOT/.env" | cut -d'=' -f2 | tr -d ' ')
elif [ -f ".env" ]; then
CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" .env | cut -d'=' -f2 | tr -d ' ')
else
CODE_REVIEW_ENABLED="false"
fi
# Normalize the value to lowercase
CODE_REVIEW_ENABLED=$(echo "$CODE_REVIEW_ENABLED" | tr '[:upper:]' '[:lower:]')
echo -e "${BLUE}️ Code Review Status: ${CODE_REVIEW_ENABLED}${NC}"
echo ""
# Get staged files (what will be committed)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(tsx?|jsx?)$' || true)
if [ "$CODE_REVIEW_ENABLED" = "true" ]; then
if [ -z "$STAGED_FILES" ]; then
echo -e "${YELLOW}⚠️ No TypeScript/JavaScript files staged to validate${NC}"
echo ""
else
echo -e "${YELLOW}🔍 Running Claude Code standards validation...${NC}"
echo ""
echo -e "${BLUE}📋 Files to validate:${NC}"
echo "$STAGED_FILES" | sed 's/^/ - /'
echo ""
echo -e "${BLUE}📤 Sending to Claude Code for validation...${NC}"
echo ""
# Build prompt with git diff of changes AND full context
VALIDATION_PROMPT=$(
cat <<'PROMPT_EOF'
You are a code reviewer for the Prowler UI project. Analyze the code changes (git diff with full context) below and validate they comply with AGENTS.md standards.
**CRITICAL: You MUST check BOTH the changed lines AND the surrounding context for violations.**
**RULES TO CHECK:**
1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }`
2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const`
3. Tailwind: NO `var()` or hex colors in className → Use `className="bg-card-bg"`
4. cn(): ONLY for conditionals → NOT for static classes
5. React 19: NO `useMemo`/`useCallback` without reason
6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod.
7. File Org: 1 feature = local, 2+ features = shared
8. Directives: Server Actions need "use server", clients need "use client"
9. Implement DRY, KISS principles. (example: reusable components, avoid repetition)
10. Layout must work for all the responsive breakpoints (mobile, tablet, desktop)
11. ANY types cannot be used - CRITICAL: Check for `: any` in all visible lines
12. Use the components inside components/shadcn if possible
13. Check Accessibility best practices (like alt tags in images, semantic HTML, Aria labels, etc.)
=== GIT DIFF WITH CONTEXT ===
PROMPT_EOF
)
# Add git diff to prompt with more context (U5 = 5 lines before/after)
VALIDATION_PROMPT="$VALIDATION_PROMPT
$(git diff --cached -U5)"
VALIDATION_PROMPT="$VALIDATION_PROMPT
=== END DIFF ===
**IMPORTANT: Your response MUST start with exactly one of these lines:**
STATUS: PASSED
STATUS: FAILED
**If FAILED:** List each violation with File, Line Number, Rule Number, and Issue.
**If PASSED:** Confirm all visible code (including context) complies with AGENTS.md standards.
**Start your response now with STATUS:**"
# Send to Claude Code
if VALIDATION_OUTPUT=$(echo "$VALIDATION_PROMPT" | claude 2>&1); then
echo "$VALIDATION_OUTPUT"
echo ""
# Check result - STRICT MODE: fail if status unclear
if echo "$VALIDATION_OUTPUT" | grep -q "^STATUS: PASSED"; then
echo ""
echo -e "${GREEN}✅ VALIDATION PASSED${NC}"
echo ""
elif echo "$VALIDATION_OUTPUT" | grep -q "^STATUS: FAILED"; then
echo ""
echo -e "${RED}❌ VALIDATION FAILED${NC}"
echo -e "${RED}Fix violations before committing${NC}"
echo ""
exit 1
else
echo ""
echo -e "${RED}❌ VALIDATION ERROR${NC}"
echo -e "${RED}Could not determine validation status from Claude Code response${NC}"
echo -e "${YELLOW}Response must start with 'STATUS: PASSED' or 'STATUS: FAILED'${NC}"
echo ""
echo -e "${YELLOW}To bypass validation temporarily, set CODE_REVIEW_ENABLED=false in .env${NC}"
echo ""
exit 1
fi
else
echo -e "${YELLOW}⚠️ Claude Code not available${NC}"
fi
echo ""
fi
else
echo -e "${YELLOW}⏭️ Code review disabled (CODE_REVIEW_ENABLED=false)${NC}"
echo ""
fi
+1
View File
@@ -9,6 +9,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- RSS feeds support [(#9109)](https://github.com/prowler-cloud/prowler/pull/9109)
- Customer Support menu item [(#9143)](https://github.com/prowler-cloud/prowler/pull/9143)
- IaC (Infrastructure as Code) provider support for scanning remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
- External resource link to IaC findings for direct navigation to source code in Git repositories [(#9151)](https://github.com/prowler-cloud/prowler/pull/9151)
### 🔄 Changed
+1
View File
@@ -10,6 +10,7 @@ WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY scripts ./scripts
RUN \
if [ -f package-lock.json ]; then npm install; \
else echo "Lockfile not found." && exit 1; \
+51
View File
@@ -87,6 +87,12 @@ You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`:
npm install
```
**Note:** The `npm install` command will automatically configure Git hooks for code quality checks. If you experience issues, you can manually configure them:
```bash
git config core.hooksPath "ui/.husky"
```
#### Run the development server
```bash
@@ -112,3 +118,48 @@ After modifying the `.npmrc` file, you need to run `pnpm install` again to ensur
- [TypeScript](https://www.typescriptlang.org/)
- [Framer Motion](https://www.framer.com/motion/)
- [next-themes](https://github.com/pacocoursey/next-themes)
## Git Hooks & Code Review
This project uses Git hooks to maintain code quality. When you commit changes to TypeScript/JavaScript files, the pre-commit hook can optionally validate them against our coding standards using Claude Code.
### Enabling Code Review
To enable automatic code review on commits, add this to your `.env` file in the project root:
```bash
CODE_REVIEW_ENABLED=true
```
When enabled, the hook will:
- ✅ Validate your staged changes against `AGENTS.md` standards
- ✅ Check for common issues (any types, incorrect imports, styling violations, etc.)
- ✅ Block commits that don't comply with the standards
- ✅ Provide helpful feedback on how to fix issues
### Disabling Code Review
To disable code review (faster commits, useful for quick iterations):
```bash
CODE_REVIEW_ENABLED=false
```
Or remove the variable from your `.env` file.
### Requirements
- [Claude Code CLI](https://github.com/anthropics/claude-code) installed and authenticated
- `.env` file in the project root with `CODE_REVIEW_ENABLED` set
### Troubleshooting
If hooks aren't running after commits:
```bash
# Verify hooks are configured
git config --get core.hooksPath # Should output: ui/.husky
# Reconfigure if needed
git config core.hooksPath "ui/.husky"
```
@@ -30,7 +30,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
m365: <M365ProviderBadge width={18} height={18} />,
github: <GitHubProviderBadge width={18} height={18} />,
iac: <IacProviderBadge width={18} height={18} />,
oci: <OracleCloudProviderBadge width={18} height={18} />,
oraclecloud: <OracleCloudProviderBadge width={18} height={18} />,
};
interface AccountsSelectorProps {
@@ -91,7 +91,7 @@ const PROVIDER_DATA: Record<
label: "Infrastructure as Code",
icon: IacProviderBadge,
},
oci: {
oraclecloud: {
label: "Oracle Cloud Infrastructure",
icon: OracleCloudProviderBadge,
},
@@ -49,7 +49,7 @@ const providerDisplayData: Record<
label: "Infrastructure as Code",
component: <CustomProviderInputIac />,
},
oci: {
oraclecloud: {
label: "Oracle Cloud Infrastructure",
component: <CustomProviderInputOracleCloud />,
},
@@ -1,18 +1,17 @@
"use client";
import { Snippet } from "@heroui/snippet";
import { Tooltip } from "@heroui/tooltip";
import { ExternalLink, Link } from "lucide-react";
import ReactMarkdown from "react-markdown";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { CustomSection } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import {
CopyLinkButton,
EntityInfoShort,
InfoField,
} from "@/components/ui/entities";
import { EntityInfoShort, InfoField } from "@/components/ui/entities";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
import { SeverityBadge } from "@/components/ui/table/severity-badge";
import { buildGitFileUrl, extractLineRangeFromUid } from "@/lib/iac-utils";
import { FindingProps, ProviderType } from "@/types";
import { Muted } from "../muted";
@@ -60,14 +59,32 @@ export const FindingDetail = ({
params.set("id", findingDetails.id);
const url = `${window.location.origin}${currentUrl.pathname}?${params.toString()}`;
// Build Git URL for IaC findings
const gitUrl =
providerDetails.provider === "iac"
? buildGitFileUrl(
providerDetails.uid,
resource.name,
extractLineRangeFromUid(attributes.uid) || "",
)
: null;
return (
<div className="flex flex-col gap-6 rounded-lg">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="dark:text-prowler-theme-pale/90 line-clamp-2 text-lg leading-tight font-medium text-gray-800">
<h2 className="dark:text-prowler-theme-pale/90 line-clamp-2 flex items-center gap-2 text-lg leading-tight font-medium text-gray-800">
{renderValue(attributes.check_metadata.checktitle)}
<CopyLinkButton url={url} />
<Tooltip content="Copy finding link to clipboard" size="sm">
<button
onClick={() => navigator.clipboard.writeText(url)}
className="text-bg-data-info inline-flex cursor-pointer transition-opacity hover:opacity-80"
aria-label="Copy finding link to clipboard"
>
<Link size={16} />
</button>
</Tooltip>
</h2>
</div>
<div className="flex items-center gap-x-4">
@@ -238,7 +255,30 @@ export const FindingDetail = ({
</CustomSection>
{/* Resource Details */}
<CustomSection title="Resource Details">
<CustomSection
title={
providerDetails.provider === "iac" ? (
<span className="flex items-center gap-2">
Resource Details
{gitUrl && (
<Tooltip content="Go to Resource in the Repository" size="sm">
<a
href={gitUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label="Open resource in repository"
>
<ExternalLink size={16} className="inline" />
</a>
</Tooltip>
)}
</span>
) : (
"Resource Details"
)
}
>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<InfoField label="Resource Name">
{renderValue(resource.name)}
@@ -41,7 +41,7 @@ export const ProvidersOverview = ({
return <GitHubProviderBadge width={30} height={30} />;
case "iac":
return <IacProviderBadge width={30} height={30} />;
case "oci":
case "oraclecloud":
return <OracleCloudProviderBadge width={30} height={30} />;
default:
return null;
@@ -56,7 +56,7 @@ export const ProvidersOverview = ({
kubernetes: "Kubernetes",
github: "GitHub",
iac: "IaC",
oci: "OCI",
oraclecloud: "OCI",
};
const providers = PROVIDER_TYPES.map((providerType) => ({
@@ -18,7 +18,7 @@ const providerTypeLabels: Record<ProviderType, string> = {
kubernetes: "Kubernetes",
github: "GitHub",
iac: "Infrastructure as Code",
oci: "Oracle Cloud Infrastructure",
oraclecloud: "Oracle Cloud Infrastructure",
};
interface EnhancedProviderSelectorProps {
@@ -88,7 +88,7 @@ export const RadioGroupProvider: React.FC<RadioGroupProviderProps> = ({
</CustomRadio>
<CustomRadio
description="Oracle Cloud Infrastructure"
value="oci"
value="oraclecloud"
>
<div className="flex items-center">
<OracleCloudProviderBadge size={26} />
@@ -167,7 +167,7 @@ export const BaseCredentialsForm = ({
control={form.control as unknown as Control<IacCredentials>}
/>
)}
{providerType === "oci" && (
{providerType === "oraclecloud" && (
<OracleCloudCredentialsForm
control={form.control as unknown as Control<OCICredentials>}
/>
@@ -56,7 +56,7 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
label: "Repository URL",
placeholder: "e.g. https://github.com/user/repo",
};
case "oci":
case "oraclecloud":
return {
label: "Tenancy OCID",
placeholder: "e.g. ocid1.tenancy.oc1..aaaaaaa...",
@@ -2,7 +2,8 @@
import { Snippet } from "@heroui/snippet";
import { Spinner } from "@heroui/spinner";
import { InfoIcon } from "lucide-react";
import { Tooltip } from "@heroui/tooltip";
import { ExternalLink, InfoIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { getFindingById } from "@/actions/findings";
@@ -17,6 +18,7 @@ import {
} from "@/components/ui/entities";
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
import { createDict } from "@/lib";
import { buildGitFileUrl } from "@/lib/iac-utils";
import { FindingProps, ProviderType, ResourceProps } from "@/types";
const renderValue = (value: string | null | undefined) => {
@@ -162,6 +164,12 @@ export const ResourceDetail = ({
const providerData = resource.relationships.provider.data.attributes;
const allFindings = findingsData;
// Build Git URL for IaC resources
const gitUrl =
providerData.provider === "iac"
? buildGitFileUrl(providerData.uid, attributes.name, "")
: null;
if (selectedFindingId) {
const findingTitle =
findingDetails?.attributes?.check_metadata?.checktitle ||
@@ -186,7 +194,30 @@ export const ResourceDetail = ({
return (
<div className="flex flex-col gap-6 rounded-lg">
{/* Resource Details section */}
<CustomSection title="Resource Details">
<CustomSection
title={
providerData.provider === "iac" ? (
<span className="flex items-center gap-2">
Resource Details
{gitUrl && (
<Tooltip content="Go to Resource in the Repository" size="sm">
<a
href={gitUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label="Open resource in repository"
>
<ExternalLink size={16} className="inline" />
</a>
</Tooltip>
)}
</span>
) : (
"Resource Details"
)
}
>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<InfoField label="Resource UID" variant="simple">
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800" hideSymbol>
+5 -3
View File
@@ -1,7 +1,9 @@
import { type ReactNode } from "react";
interface CustomSectionProps {
title: string;
children: React.ReactNode;
action?: React.ReactNode;
title: string | ReactNode;
children: ReactNode;
action?: ReactNode;
}
export const CustomSection = ({
@@ -1,41 +0,0 @@
"use client";
import { Tooltip } from "@heroui/tooltip";
import { CheckCheck, ExternalLink } from "lucide-react";
import { useState } from "react";
type CopyLinkButtonProps = {
url: string;
};
export const CopyLinkButton = ({ url }: CopyLinkButtonProps) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 500);
} catch (err) {
console.error("Failed to copy URL to clipboard:", err);
}
};
return (
<Tooltip content="Copy URL to clipboard" size="sm">
<button
type="button"
onClick={handleCopy}
className="ml-2 inline-flex cursor-pointer flex-row items-center gap-2 p-0"
aria-label="Copy URL to clipboard"
>
{copied ? (
<CheckCheck size={16} className="inline" />
) : (
<ExternalLink size={16} className="inline" />
)}
</button>
</Tooltip>
);
};
@@ -28,7 +28,7 @@ export const getProviderLogo = (provider: ProviderType) => {
return <GitHubProviderBadge width={35} height={35} />;
case "iac":
return <IacProviderBadge width={35} height={35} />;
case "oci":
case "oraclecloud":
return <OracleCloudProviderBadge width={35} height={35} />;
default:
return null;
@@ -51,7 +51,7 @@ export const getProviderName = (provider: ProviderType): string => {
return "GitHub";
case "iac":
return "Infrastructure as Code";
case "oci":
case "oraclecloud":
return "Oracle Cloud Infrastructure";
default:
return "Unknown Provider";
-1
View File
@@ -1,4 +1,3 @@
export * from "./copy-link-button";
export * from "./date-with-time";
export * from "./entity-info-short";
export * from "./get-provider-logo";
@@ -0,0 +1,115 @@
# Code Review - Quick Start
## 3 Steps to Enable
### 1. Open `.env`
```bash
nano ui/.env
# or your favorite editor
```
### 2. Find this line
```bash
CODE_REVIEW_ENABLED=false
```
### 3. Change it to
```bash
CODE_REVIEW_ENABLED=true
```
**Done! ✅**
---
## What Happens Now
Every time you `git commit`:
```
✅ If your code complies with AGENTS.md standards:
→ Commit executes normally
❌ If there are standard violations:
→ Commit is BLOCKED
→ You see the errors in the terminal
→ Fix the code
→ Commit again
```
---
## Example
```bash
$ git commit -m "feat: add new component"
🏁 Prowler UI - Pre-Commit Hook
️ Code Review Status: true
🔍 Running Claude Code standards validation...
📋 Files to validate:
- components/my-feature.tsx
📤 Sending to Claude Code for validation...
STATUS: FAILED
- File: components/my-feature.tsx:45
Rule: React Imports
Issue: Using 'import * as React'
Expected: import { useState } from "react"
❌ VALIDATION FAILED
Fix violations before committing
# Fix the file and commit again
$ git commit -m "feat: add new component"
🏁 Prowler UI - Pre-Commit Hook
️ Code Review Status: true
🔍 Running Claude Code standards validation...
✅ VALIDATION PASSED
# Commit successful ✅
```
---
## Disable Temporarily
If you need to commit without validation:
```bash
# Option 1: Change in .env
CODE_REVIEW_ENABLED=false
# Option 2: Bypass (use with caution!)
git commit --no-verify
```
---
## What Gets Validated
- ✅ Correct React imports
- ✅ TypeScript patterns (const-based types)
- ✅ Tailwind CSS (no var() or hex in className)
- ✅ cn() utility (only for conditionals)
- ✅ No useMemo/useCallback without reason
- ✅ Zod v4 syntax
- ✅ File organization
- ✅ Directives "use client"/"use server"
---
## More Info
Read `CODE_REVIEW_SETUP.md` for:
- Troubleshooting
- Complete details
- Advanced configuration
+296
View File
@@ -0,0 +1,296 @@
# Code Review Setup - Prowler UI
Guide to set up automatic code validation with Claude Code in the pre-commit hook.
## Overview
The code review system works like this:
1. **When you enable `CODE_REVIEW_ENABLED=true` in `.env`**
- When you `git commit`, the pre-commit hook runs
- Only validates TypeScript/JavaScript files you're committing
- Uses Claude Code to check if they comply with AGENTS.md
- If there are violations → **BLOCKS the commit**
- If everything is fine → Continues normally
2. **When `CODE_REVIEW_ENABLED=false` (default)**
- The pre-commit hook does not run validation
- No standards validation
- Developers can commit without restrictions
## Installation
### 1. Ensure Claude Code is in your PATH
```bash
# Verify that claude is available in terminal
which claude
# If it doesn't appear, check your Claude Code CLI installation
```
### 2. Enable validation in `.env`
In `/ui/.env`, find the "Code Review Configuration" section:
```bash
#### Code Review Configuration ####
# Enable Claude Code standards validation on pre-commit hook
# Set to 'true' to validate changes against AGENTS.md standards via Claude Code
# Set to 'false' to skip validation
CODE_REVIEW_ENABLED=false # ← Change this to 'true'
```
**Options:**
- `CODE_REVIEW_ENABLED=true` → Enables validation
- `CODE_REVIEW_ENABLED=false` → Disables validation (default)
### 3. The hook is ready
The `.husky/pre-commit` file already contains the logic. You don't need to install anything else.
## How It Works
### Normal Flow (with validation enabled)
```bash
$ git commit -m "feat: add new component"
# Pre-commit hook executes automatically
🚀 Prowler UI - Pre-Commit Hook
️ Code Review Status: true
📋 Files to validate:
- components/new-feature.tsx
- types/new-feature.ts
📤 Sending to Claude Code for validation...
# Claude analyzes the files...
=== VALIDATION REPORT ===
STATUS: PASSED
All files comply with AGENTS.md standards.
✅ VALIDATION PASSED
# Commit continues ✅
```
### If There Are Violations
```bash
$ git commit -m "feat: add new component"
# Claude detects issues...
=== VALIDATION REPORT ===
STATUS: FAILED
- File: components/new-feature.tsx:15
Rule: React Imports
Issue: Using 'import * as React' instead of named imports
Expected: import { useState } from "react"
❌ VALIDATION FAILED
Please fix the violations before committing:
1. Review the violations listed above
2. Fix the code according to AGENTS.md standards
3. Commit your changes
4. Try again
# Commit is BLOCKED ❌
```
## What Gets Validated
The system verifies that files comply with:
### 1. React Imports
```typescript
// ❌ WRONG
import * as React from "react"
import React, { useState } from "react"
// ✅ CORRECT
import { useState } from "react"
```
### 2. TypeScript Type Patterns
```typescript
// ❌ WRONG
type SortOption = "high-low" | "low-high"
// ✅ CORRECT
const SORT_OPTIONS = {
HIGH_LOW: "high-low",
LOW_HIGH: "low-high",
} as const
type SortOption = typeof SORT_OPTIONS[keyof typeof SORT_OPTIONS]
```
### 3. Tailwind CSS
```typescript
// ❌ WRONG
className="bg-[var(--color)]"
className="text-[#ffffff]"
// ✅ CORRECT
className="bg-card-bg text-white"
```
### 4. cn() Utility
```typescript
// ❌ WRONG
className={cn("flex items-center")}
// ✅ CORRECT
className={cn("h-3 w-3", isCircle ? "rounded-full" : "rounded-sm")}
```
### 5. React 19 Hooks
```typescript
// ❌ WRONG
const memoized = useMemo(() => value, [])
// ✅ CORRECT
// Don't use useMemo (React Compiler handles it)
const value = expensiveCalculation()
```
### 6. Zod v4 Syntax
```typescript
// ❌ WRONG
z.string().email()
z.string().nonempty()
// ✅ CORRECT
z.email()
z.string().min(1)
```
### 7. File Organization
```
// ❌ WRONG
Code used by 2+ features in feature-specific folder
// ✅ CORRECT
Code used by 1 feature → local in that feature
Code used by 2+ features → in shared/global
```
### 8. Use Directives
```typescript
// ❌ WRONG
export async function updateUser() { } // Missing "use server"
// ✅ CORRECT
"use server"
export async function updateUser() { }
```
## Disable Temporarily
If you need to commit without validation temporarily:
```bash
# Option 1: Change in .env
CODE_REVIEW_ENABLED=false
git commit
# Option 2: Use git hook bypass
git commit --no-verify
# Option 3: Disable the hook
chmod -x .husky/pre-commit
git commit
chmod +x .husky/pre-commit
```
**⚠️ Note:** `--no-verify` skips ALL hooks.
## Troubleshooting
### "Claude Code CLI not found"
```
⚠️ Claude Code CLI not found in PATH
To enable: ensure Claude Code is in PATH and CODE_REVIEW_ENABLED=true
```
**Solution:**
```bash
# Check where claude-code is installed
which claude-code
# If not found, add to your ~/.zshrc:
export PATH="$HOME/.local/bin:$PATH" # or where it's installed
# Reload the terminal
source ~/.zshrc
```
### "Validation inconclusive"
If Claude Code cannot determine the status:
```
⚠️ Could not determine validation status
Allowing commit (validation inconclusive)
```
The commit is allowed automatically. If you want to be stricter, you can:
1. Manually review files against AGENTS.md
2. Report the analysis problem to Claude
### Build fails after validation
```
❌ Build failed
```
If validation passes but build fails:
1. Check the build error
2. Fix it locally
3. Commit and try again
## View the Full Report
Reports are saved in temporary files that are deleted afterward. To see the detailed report in real-time, watch the hook output:
```bash
git commit 2>&1 | tee commit-report.txt
```
This will save everything to `commit-report.txt`.
## For the Team
### Enable on your machine
```bash
cd ui
# Edit .env locally and set:
CODE_REVIEW_ENABLED=true
```
### Recommended Flow
1. **During development**: `CODE_REVIEW_ENABLED=false`
- Iterate faster
- Build check still runs
2. **Before final commit**: `CODE_REVIEW_ENABLED=true`
- Verify you meet standards
- Prevent PRs rejected for violations
3. **In CI/CD**: You could add additional validation
- (future) Server-side validation in GitHub Actions
## Questions?
If you have questions about the standards being validated, check:
- `AGENTS.md` - Complete architecture guide
- `CLAUDE.md` - Project-specific instructions
+241
View File
@@ -0,0 +1,241 @@
# Code Review System Documentation
Complete documentation for the Claude Code-powered pre-commit validation system.
## Quick Navigation
**Want to get started in 3 steps?**
→ Read: [`CODE_REVIEW_QUICK_START.md`](./CODE_REVIEW_QUICK_START.md)
**Want complete technical details?**
→ Read: [`CODE_REVIEW_SETUP.md`](./CODE_REVIEW_SETUP.md)
---
## What This System Does
Automatically validates code against AGENTS.md standards when you commit using Claude Code.
```
git commit
(Optional) Claude Code validation
If violations found → Commit is BLOCKED ❌
If code complies → Commit continues ✅
```
**Key Feature:** Configurable with a single variable in `.env`
- `CODE_REVIEW_ENABLED=true` → Validates (recommended before commits)
- `CODE_REVIEW_ENABLED=false` → Skip validation (default, for iteration)
---
## File Guide
| File | Purpose | Read Time |
|------|---------|-----------|
| [`CODE_REVIEW_QUICK_START.md`](./CODE_REVIEW_QUICK_START.md) | 3-step setup & examples | 5 min |
| [`CODE_REVIEW_SETUP.md`](./CODE_REVIEW_SETUP.md) | Complete technical guide | 15 min |
---
## What Gets Validated
When validation is enabled, the system checks:
✅ **React Imports**
- Must use: `import { useState } from "react"`
- Not: `import * as React` or `import React, {`
✅ **TypeScript Types**
- Must use: `const STATUS = {...} as const; type Status = typeof STATUS[...]`
- Not: `type Status = "a" | "b"`
✅ **Tailwind CSS**
- Must use: `className="bg-card-bg text-white"`
- Not: `className="bg-[var(...)]"` or `className="text-[#fff]"`
✅ **cn() Utility**
- Must use for: `cn("h-3", isActive && "bg-blue")`
- Not for: `cn("static-class")`
✅ **React 19 Hooks**
- No: `useMemo()` / `useCallback()` without documented reason
- Use: Nothing (React Compiler handles optimization)
✅ **Zod v4 Syntax**
- Must use: `z.email()`, `.min(1)`
- Not: `z.string().email()`, `.nonempty()`
✅ **File Organization**
- 1 feature uses → Keep local in feature folder
- 2+ features use → Move to shared/global
✅ **Directives**
- Server Actions must have: `"use server"`
- Client Components must have: `"use client"`
---
## Installation (For Your Team)
### Step 1: Decide if you want validation
- **Optional:** Each developer decides
- **Team policy:** Consider making it standard before commits
### Step 2: Enable in your environment
```bash
# Edit ui/.env
CODE_REVIEW_ENABLED=true
```
### Step 3: Done!
Your next `git commit` will validate automatically.
---
## Support
| Question | Answer |
|----------|--------|
| How do I enable it? | Change `CODE_REVIEW_ENABLED=true` in `.env` |
| How do I disable it? | Change `CODE_REVIEW_ENABLED=false` in `.env` |
| How do I bypass? | Use `git commit --no-verify` (emergency only) |
| What if Claude Code isn't found? | Check PATH: `which claude` |
| What if hook doesn't run? | Check executable: `chmod +x .husky/pre-commit` |
| How do I test it? | Enable validation and commit code with violations to test |
| What if I don't have Claude Code? | Validation is skipped gracefully |
---
## Key Features
✅ **No Setup Required**
- Uses Claude Code already in your PATH
- No API keys needed
- Works offline (if Claude Code supports it)
✅ **Smart Validation**
- Only checks files being committed
- Not the entire codebase
- Fast: ~10-30 seconds with validation enabled
✅ **Flexible**
- Can be enabled/disabled per developer
- Can be disabled temporarily with `git commit --no-verify`
- Default is disabled (doesn't interrupt workflow)
✅ **Clear Feedback**
- Shows exactly what violates standards
- Shows file:line references
- Explains how to fix each issue
✅ **Well Documented**
- 5 different documentation files
- For different needs and levels
- Examples and troubleshooting included
---
## Architecture
```
┌─────────────────────────────────────────┐
│ Developer commits code │
└────────────────┬────────────────────────┘
┌─────────────────┐
│ Pre-Commit Hook │
│ (.husky/pre-commit)
└────────┬────────┘
Read CODE_REVIEW_ENABLED from .env
┌──────────────────────────┐
│ If false (disabled) │
└────────┬─────────────────┘
exit 0 (OK)
Commit continues ✅
┌──────────────────────────┐
│ If true (enabled) │
└────────┬─────────────────┘
Extract staged files
(git diff --cached)
Build prompt with git diff
Send to: claude < prompt
Analyze against AGENTS.md
Return: STATUS: PASSED or FAILED
Parse with: grep "^STATUS:"
┌──────────────────┐
│ PASSED detected │
└────────┬─────────┘
exit 0 (OK)
Commit continues ✅
┌──────────────────┐
│ FAILED detected │
└────────┬─────────┘
Show violations
exit 1 (FAIL)
Commit is BLOCKED ❌
Developer fixes code
Developer commits again
```
---
## Getting Started
1. **Read:** [`CODE_REVIEW_QUICK_START.md`](./CODE_REVIEW_QUICK_START.md) (5 minutes)
2. **Enable:** Set `CODE_REVIEW_ENABLED=true` in your `ui/.env`
3. **Test:** Commit some code and see validation in action
4. **For help:** See the troubleshooting section in [`CODE_REVIEW_SETUP.md`](./CODE_REVIEW_SETUP.md)
---
## Implementation Details
- **Files Modified:** 1 (`.husky/pre-commit`)
- **Files Created:** 3 (documentation)
- **Hook Size:** ~120 lines of bash
- **Dependencies:** Claude Code CLI (already available)
- **Setup Time:** 1 minute
- **Default:** Disabled (no workflow interruption)
---
## Questions?
- **How to enable?**`CODE_REVIEW_QUICK_START.md`
- **How does it work?**`CODE_REVIEW_SETUP.md`
- **Troubleshooting?** → See troubleshooting section in `CODE_REVIEW_SETUP.md`
---
## Status
✅ **Ready to Use**
The system is fully implemented, documented, and tested. You can enable it immediately with a single variable change.
---
**Last Updated:** November 6, 2024
**Status:** Complete Implementation
+1 -1
View File
@@ -157,7 +157,7 @@ export const useCredentialsForm = ({
};
}
return baseDefaults;
case "oci":
case "oraclecloud":
return {
...baseDefaults,
[ProviderCredentialFields.OCI_USER]: "",
+2 -2
View File
@@ -37,10 +37,10 @@ export const getProviderHelpText = (provider: string) => {
text: "Need help scanning your Infrastructure as Code repository?",
link: "https://goto.prowler.com/provider-iac",
};
case "oci":
case "oraclecloud":
return {
text: "Need help connecting your Oracle Cloud account?",
link: "https://goto.prowler.com/provider-oci",
link: "https://goto.prowler.com/provider-oraclecloud",
};
default:
return {
+188
View File
@@ -0,0 +1,188 @@
/**
* Extracts line range from a Finding UID
* Finding UID format: {CheckID}-{resource_name}-{line_range}
* Example: "AVD-AWS-0001-main.tf-10:15" -> "10:15"
*
* @param findingUid - The finding UID
* @returns Line range string or null if not found
*/
export function extractLineRangeFromUid(findingUid: string): string | null {
if (!findingUid) {
return null;
}
// Split by dash and get the last part (line range)
const parts = findingUid.split("-");
const lastPart = parts[parts.length - 1];
// Check if the last part is a line range in format "number:number"
// This ensures we don't confuse numeric filenames with line ranges
if (/^\d+:\d+$/.test(lastPart)) {
return lastPart;
}
return null;
}
/**
* Builds a Git repository URL with file path and line numbers
* Supports GitHub, GitLab, Bitbucket, and generic Git URLs
*
* @param repoUrl - Repository URL (can be HTTPS or git@ format)
* @param filePath - Path to the file in the repository
* @param lineRange - Line range in format "10-15" or "10:15" or "10"
* @returns Complete URL to the file with line numbers, or null if URL cannot be built
*/
export function buildGitFileUrl(
repoUrl: string,
filePath: string,
lineRange: string,
): string | null {
if (!repoUrl || !filePath) {
return null;
}
try {
// Normalize the repository URL
let normalizedUrl = repoUrl.trim();
// Convert git@ format to HTTPS (best effort)
if (normalizedUrl.startsWith("git@")) {
// git@github.com:user/repo.git -> https://github.com/user/repo
normalizedUrl = normalizedUrl
.replace(/^git@/, "https://")
.replace(/\.git$/, "")
.replace(/:([^:]+)$/, "/$1"); // Replace last : with /
}
// Remove .git suffix if present
normalizedUrl = normalizedUrl.replace(/\.git$/, "");
// Parse URL to determine provider
const url = new URL(normalizedUrl);
const hostname = url.hostname.toLowerCase();
// Clean up file path (remove leading slashes)
const cleanFilePath = filePath.replace(/^\/+/, "");
// Parse line range
const { startLine, endLine } = parseLineRange(lineRange);
// Build URL based on Git provider
if (hostname.includes("github")) {
return buildGitHubUrl(normalizedUrl, cleanFilePath, startLine, endLine);
} else if (hostname.includes("gitlab")) {
return buildGitLabUrl(normalizedUrl, cleanFilePath, startLine, endLine);
} else if (hostname.includes("bitbucket")) {
return buildBitbucketUrl(
normalizedUrl,
cleanFilePath,
startLine,
endLine,
);
} else {
// Generic Git provider - try GitHub format as fallback
return buildGitHubUrl(normalizedUrl, cleanFilePath, startLine, endLine);
}
} catch (error) {
console.error("Error building Git file URL:", error);
return null;
}
}
/**
* Parses line range string into start and end line numbers
*/
function parseLineRange(lineRange: string): {
startLine: number | null;
endLine: number | null;
} {
if (!lineRange || lineRange === "file") {
return { startLine: null, endLine: null };
}
// Handle formats: "10-15", "10:15", "10"
// Safe regex: anchored pattern for line numbers only (no ReDoS risk)
// eslint-disable-next-line security/detect-unsafe-regex
const match = lineRange.match(/^(\d+)[-:]?(\d+)?$/);
if (match) {
const startLine = parseInt(match[1], 10);
const endLine = match[2] ? parseInt(match[2], 10) : startLine;
return { startLine, endLine };
}
return { startLine: null, endLine: null };
}
/**
* Builds GitHub-style URL
* Format: https://github.com/user/repo/blob/main/path/file.tf#L10-L15
*/
function buildGitHubUrl(
baseUrl: string,
filePath: string,
startLine: number | null,
endLine: number | null,
): string {
// Assume main/master branch for simplicity
const branch = "main";
let url = `${baseUrl}/blob/${branch}/${filePath}`;
if (startLine !== null) {
if (endLine !== null && endLine !== startLine) {
url += `#L${startLine}-L${endLine}`;
} else {
url += `#L${startLine}`;
}
}
return url;
}
/**
* Builds GitLab-style URL
* Format: https://gitlab.com/user/repo/-/blob/main/path/file.tf#L10-15
*/
function buildGitLabUrl(
baseUrl: string,
filePath: string,
startLine: number | null,
endLine: number | null,
): string {
const branch = "main";
let url = `${baseUrl}/-/blob/${branch}/${filePath}`;
if (startLine !== null) {
if (endLine !== null && endLine !== startLine) {
url += `#L${startLine}-${endLine}`;
} else {
url += `#L${startLine}`;
}
}
return url;
}
/**
* Builds Bitbucket-style URL
* Format: https://bitbucket.org/user/repo/src/main/path/file.tf#lines-10:15
*/
function buildBitbucketUrl(
baseUrl: string,
filePath: string,
startLine: number | null,
endLine: number | null,
): string {
const branch = "main";
let url = `${baseUrl}/src/${branch}/${filePath}`;
if (startLine !== null) {
if (endLine !== null && endLine !== startLine) {
url += `#lines-${startLine}:${endLine}`;
} else {
url += `#lines-${startLine}`;
}
}
return url;
}
@@ -304,7 +304,7 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildIacSecret(formData),
}),
oci: () => ({
oraclecloud: () => ({
secretType: "static",
secret: buildOracleCloudSecret(formData, providerUid),
}),
+1 -1
View File
@@ -7,7 +7,7 @@
"start": "next start",
"start:standalone": "node .next/standalone/server.js",
"deps:log": "node scripts/update-dependency-log.js",
"postinstall": "node -e \"const fs=require('fs'); if(fs.existsSync('scripts/update-dependency-log.js')) require('./scripts/update-dependency-log.js'); else console.log('skip deps:log (script missing)');\"",
"postinstall": "node scripts/postinstall.js",
"typecheck": "tsc",
"healthcheck": "npm run typecheck && npm run lint:check",
"lint:check": "eslint . --ext .ts,.tsx -c .eslintrc.cjs",
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* Post-install script for Prowler UI
*
* This script runs after npm install to:
* 1. Update dependency log (if the script exists)
* 2. Setup git hooks (if the script exists)
*/
const fs = require("fs");
const path = require("path");
function runScriptIfExists(scriptPath, scriptName) {
const fullPath = path.join(__dirname, scriptPath);
if (fs.existsSync(fullPath)) {
try {
require(fullPath);
} catch (error) {
console.warn(`⚠️ Error running ${scriptName}:`, error.message);
}
} else {
console.log(`Skip ${scriptName} (script missing)`);
}
}
// Run dependency log update
runScriptIfExists("./update-dependency-log.js", "deps:log");
// Run git hooks setup
runScriptIfExists("./setup-git-hooks.js", "setup-git-hooks");
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/**
* Setup Git Hooks for Prowler UI
*
* This script configures Git to use Husky hooks located in ui/.husky
* It runs automatically after npm install via the postinstall script.
*/
const { execSync } = require('child_process');
const path = require('path');
try {
// Get the git root directory
const gitRoot = execSync('git rev-parse --show-toplevel', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
// Check if we're in a git repository
if (!gitRoot) {
console.log('⚠️ Not in a git repository. Skipping git hooks setup.');
process.exit(0);
}
// Get current hooks path
let currentHooksPath;
try {
currentHooksPath = execSync('git config --get core.hooksPath', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
} catch {
// core.hooksPath not set yet
currentHooksPath = null;
}
const expectedHooksPath = 'ui/.husky';
// Only configure if not already set correctly
if (currentHooksPath !== expectedHooksPath) {
execSync(`git config core.hooksPath "${expectedHooksPath}"`, {
stdio: 'inherit'
});
console.log('✅ Git hooks configured: core.hooksPath = ui/.husky');
} else {
console.log('✅ Git hooks already configured correctly');
}
} catch (error) {
// Don't fail the installation if git hooks setup fails
console.warn('⚠️ Could not setup git hooks:', error.message);
console.warn(' You may need to run manually: git config core.hooksPath "ui/.husky"');
}
+3 -2
View File
@@ -4,9 +4,10 @@ const { heroui } = require("@heroui/theme");
module.exports = {
darkMode: ["class"],
content: [
"./components/**/*.{ts,jsx,tsx,mdx}",
"./app/**/*.{ts,jsx,tsx,mdx}",
"./components/**/*.{ts,jsx,tsx}",
"./app/**/*.{ts,jsx,tsx}",
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
"!./docs/**/*",
],
prefix: "",
theme: {
+2 -2
View File
@@ -116,7 +116,7 @@ export const addProviderFormSchema = z
providerUid: z.string(),
}),
z.object({
providerType: z.literal("oci"),
providerType: z.literal("oraclecloud"),
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
@@ -210,7 +210,7 @@ export const addCredentialsFormSchema = (
.string()
.optional(),
}
: providerType === "oci"
: providerType === "oraclecloud"
? {
[ProviderCredentialFields.OCI_USER]: z
.string()
+1 -1
View File
@@ -6,7 +6,7 @@ export const PROVIDER_TYPES = [
"m365",
"github",
"iac",
"oci",
"oraclecloud",
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];