mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 14:54:30 +00:00
Merge remote-tracking branch 'origin/master' into feat/PROWLER-939-stage-2-registry-auth
# Conflicts: # prowler/CHANGELOG.md # prowler/lib/check/models.py # prowler/providers/common/provider.py # prowler/providers/image/exceptions/exceptions.py # prowler/providers/image/image_provider.py # tests/providers/image/image_provider_test.py
This commit is contained in:
68 files changed
+28314
-1246
No files matched your search
@@ -51,18 +51,16 @@ jobs:
|
||||
"amitsharm"
|
||||
"andoniaf"
|
||||
"cesararroba"
|
||||
"Chan9390"
|
||||
"danibarranqueroo"
|
||||
"HugoPBrito"
|
||||
"jfagoagas"
|
||||
"josema-xyz"
|
||||
"lydiavilchez"
|
||||
"mmuller88"
|
||||
"MrCloudSec"
|
||||
# "MrCloudSec"
|
||||
"pedrooot"
|
||||
"prowler-bot"
|
||||
"puchy22"
|
||||
"rakan-pro"
|
||||
"RosaRivasProwler"
|
||||
"StylusFrost"
|
||||
"toniblyx"
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
name: 'SDK: Refresh OCI Regions'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * 1' # Every Monday at 09:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: '3.12'
|
||||
|
||||
jobs:
|
||||
refresh-oci-regions:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: 'master'
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install oci
|
||||
|
||||
- name: Update OCI regions
|
||||
env:
|
||||
OCI_CLI_USER: ${{ secrets.E2E_OCI_USER_ID }}
|
||||
OCI_CLI_FINGERPRINT: ${{ secrets.E2E_OCI_FINGERPRINT }}
|
||||
OCI_CLI_TENANCY: ${{ secrets.E2E_OCI_TENANCY_ID }}
|
||||
OCI_CLI_KEY_CONTENT: ${{ secrets.E2E_OCI_KEY_CONTENT }}
|
||||
OCI_CLI_REGION: ${{ secrets.E2E_OCI_REGION }}
|
||||
run: python util/update_oci_regions.py
|
||||
|
||||
- name: Create pull request
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>'
|
||||
committer: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'
|
||||
commit-message: 'feat(oraclecloud): update commercial regions'
|
||||
branch: 'oci-regions-update-${{ github.run_number }}'
|
||||
title: 'feat(oraclecloud): Update commercial regions'
|
||||
labels: |
|
||||
status/waiting-for-revision
|
||||
severity/low
|
||||
provider/oraclecloud
|
||||
no-changelog
|
||||
body: |
|
||||
### Description
|
||||
|
||||
Automated update of OCI commercial regions from the official Oracle Cloud Infrastructure Identity service.
|
||||
|
||||
**Trigger:** ${{ github.event_name == 'schedule' && 'Scheduled (weekly)' || github.event_name == 'workflow_dispatch' && 'Manual' || 'Workflow update' }}
|
||||
**Run:** [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
### Changes
|
||||
|
||||
This PR updates the `OCI_COMMERCIAL_REGIONS` dictionary in `prowler/providers/oraclecloud/config.py` with the latest regions fetched from the OCI Identity API (`list_regions()`).
|
||||
|
||||
- Government regions (`OCI_GOVERNMENT_REGIONS`) are preserved unchanged
|
||||
- Region display names are mapped from Oracle's official documentation
|
||||
|
||||
### Checklist
|
||||
|
||||
- [x] This is an automated update from OCI official sources
|
||||
- [x] Government regions (us-langley-1, us-luke-1) preserved
|
||||
- [x] No manual review of region data required
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
|
||||
- name: PR creation result
|
||||
run: |
|
||||
if [[ "${{ steps.create-pr.outputs.pull-request-number }}" ]]; then
|
||||
echo "✓ Pull request #${{ steps.create-pr.outputs.pull-request-number }} created successfully"
|
||||
echo "URL: ${{ steps.create-pr.outputs.pull-request-url }}"
|
||||
else
|
||||
echo "✓ No changes detected - OCI regions are up to date"
|
||||
fi
|
||||
@@ -4,10 +4,22 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.20.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- OpenStack provider support [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
|
||||
- Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992)
|
||||
- Attack Paths: Add full paths set from [pathfinding.cloud](https://pathfinding.cloud/) [(#10008)](https://github.com/prowler-cloud/prowler/pull/10008)
|
||||
- Support CSA CCM for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
|
||||
- Support CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
|
||||
- Support CSA CCM for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Pillow 12.1.1 (CVE-2021-25289) [(#10027)](https://github.com/prowler-cloud/prowler/pull/10027)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+93
-93
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "about-time"
|
||||
@@ -6258,103 +6258,103 @@ urllib3 = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.0"
|
||||
version = "12.1.1"
|
||||
description = "Python Imaging Library (fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"},
|
||||
{file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"},
|
||||
{file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"},
|
||||
{file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"},
|
||||
{file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"},
|
||||
{file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"},
|
||||
{file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"},
|
||||
{file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"},
|
||||
{file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"},
|
||||
{file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"},
|
||||
{file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,39 @@
|
||||
# Generated by Django migration for OpenStack provider support
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import api.db_utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0075_cloudflare_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"),
|
||||
("alibabacloud", "Alibaba Cloud"),
|
||||
("cloudflare", "Cloudflare"),
|
||||
("openstack", "OpenStack"),
|
||||
],
|
||||
default="aws",
|
||||
),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'openstack';",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
]
|
||||
@@ -288,6 +288,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
ORACLECLOUD = "oraclecloud", _("Oracle Cloud Infrastructure")
|
||||
ALIBABACLOUD = "alibabacloud", _("Alibaba Cloud")
|
||||
CLOUDFLARE = "cloudflare", _("Cloudflare")
|
||||
OPENSTACK = "openstack", _("OpenStack")
|
||||
|
||||
@staticmethod
|
||||
def validate_aws_uid(value):
|
||||
@@ -410,6 +411,15 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_openstack_uid(value):
|
||||
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$", value):
|
||||
raise ModelValidationError(
|
||||
detail="OpenStack provider ID must be a valid project ID (UUID or project name).",
|
||||
code="openstack-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)
|
||||
|
||||
@@ -367,6 +367,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -380,6 +381,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -398,6 +400,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -413,6 +416,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -1348,6 +1352,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -1361,6 +1366,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -1379,6 +1385,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -1394,6 +1401,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -1938,6 +1946,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -1951,6 +1960,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -1969,6 +1979,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -1984,6 +1995,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -2436,6 +2448,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -2449,6 +2462,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -2467,6 +2481,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -2482,6 +2497,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -2932,6 +2948,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -2945,6 +2962,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -2963,6 +2981,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -2978,6 +2997,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -3416,6 +3436,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -3429,6 +3450,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -3447,6 +3469,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -3462,6 +3485,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -5241,6 +5265,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -5254,6 +5279,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5272,6 +5298,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -5287,6 +5314,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -5404,6 +5432,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -5417,6 +5446,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5435,6 +5465,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -5450,6 +5481,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -5556,6 +5588,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -5569,6 +5602,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5586,6 +5620,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -5601,6 +5636,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -5739,6 +5775,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -5752,6 +5789,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5770,6 +5808,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -5785,6 +5824,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -5936,6 +5976,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -5949,6 +5990,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -5967,6 +6009,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -5982,6 +6025,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -6127,6 +6171,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -6140,6 +6185,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6157,6 +6203,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -6172,6 +6219,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -6359,6 +6407,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -6372,6 +6421,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6390,6 +6440,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -6405,6 +6456,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -6521,6 +6573,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -6534,6 +6587,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6552,6 +6606,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -6567,6 +6622,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -6707,6 +6763,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -6720,6 +6777,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -6738,6 +6796,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -6753,6 +6812,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -7534,6 +7594,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -7547,6 +7608,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider__in]
|
||||
schema:
|
||||
@@ -7565,6 +7627,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -7580,6 +7643,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -7598,6 +7662,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -7611,6 +7676,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -7629,6 +7695,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -7644,6 +7711,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
@@ -8285,6 +8353,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -8298,6 +8367,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -8316,6 +8386,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -8331,6 +8402,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -8787,6 +8859,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -8800,6 +8873,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -8818,6 +8892,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -8833,6 +8908,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -9102,6 +9178,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -9115,6 +9192,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -9133,6 +9211,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -9148,6 +9227,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -9423,6 +9503,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -9436,6 +9517,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -9454,6 +9536,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -9469,6 +9552,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -10278,6 +10362,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -10291,6 +10376,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
@@ -10309,6 +10395,7 @@ paths:
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- openstack
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
@@ -10324,6 +10411,7 @@ paths:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
@@ -17348,6 +17436,50 @@ components:
|
||||
required:
|
||||
- api_key
|
||||
- api_email
|
||||
- type: object
|
||||
title: OpenStack clouds.yaml Credentials
|
||||
properties:
|
||||
clouds_yaml_content:
|
||||
type: string
|
||||
description: The full content of a clouds.yaml configuration
|
||||
file.
|
||||
clouds_yaml_cloud:
|
||||
type: string
|
||||
description: The name of the cloud to use from the clouds.yaml
|
||||
file.
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g.,
|
||||
https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: Keystone API version (default: 3).
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: User domain name (default: Default).
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: Project domain name (default: Default).
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret
|
||||
@@ -18347,6 +18479,7 @@ components:
|
||||
- oraclecloud
|
||||
- alibabacloud
|
||||
- cloudflare
|
||||
- openstack
|
||||
type: string
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
@@ -18360,6 +18493,7 @@ components:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
uid:
|
||||
type: string
|
||||
@@ -18477,6 +18611,7 @@ components:
|
||||
- oraclecloud
|
||||
- alibabacloud
|
||||
- cloudflare
|
||||
- openstack
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
description: |-
|
||||
@@ -18493,6 +18628,7 @@ components:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
@@ -18541,6 +18677,7 @@ components:
|
||||
- oraclecloud
|
||||
- alibabacloud
|
||||
- cloudflare
|
||||
- openstack
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
description: |-
|
||||
@@ -18557,6 +18694,7 @@ components:
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
uid:
|
||||
type: string
|
||||
minLength: 3
|
||||
@@ -19378,6 +19516,48 @@ components:
|
||||
required:
|
||||
- api_key
|
||||
- api_email
|
||||
- type: object
|
||||
title: OpenStack clouds.yaml Credentials
|
||||
properties:
|
||||
clouds_yaml_content:
|
||||
type: string
|
||||
description: The full content of a clouds.yaml configuration file.
|
||||
clouds_yaml_cloud:
|
||||
type: string
|
||||
description: The name of the cloud to use from the clouds.yaml
|
||||
file.
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g., https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: Keystone API version (default: 3).
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: User domain name (default: Default).
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: Project domain name (default: Default).
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret_type
|
||||
@@ -19764,6 +19944,50 @@ components:
|
||||
required:
|
||||
- api_key
|
||||
- api_email
|
||||
- type: object
|
||||
title: OpenStack clouds.yaml Credentials
|
||||
properties:
|
||||
clouds_yaml_content:
|
||||
type: string
|
||||
description: The full content of a clouds.yaml configuration
|
||||
file.
|
||||
clouds_yaml_cloud:
|
||||
type: string
|
||||
description: The name of the cloud to use from the clouds.yaml
|
||||
file.
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g.,
|
||||
https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: Keystone API version (default: 3).
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: User domain name (default: Default).
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: Project domain name (default: Default).
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret_type
|
||||
@@ -20162,6 +20386,48 @@ components:
|
||||
required:
|
||||
- api_key
|
||||
- api_email
|
||||
- type: object
|
||||
title: OpenStack clouds.yaml Credentials
|
||||
properties:
|
||||
clouds_yaml_content:
|
||||
type: string
|
||||
description: The full content of a clouds.yaml configuration file.
|
||||
clouds_yaml_cloud:
|
||||
type: string
|
||||
description: The name of the cloud to use from the clouds.yaml
|
||||
file.
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g., https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: Keystone API version (default: 3).
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: User domain name (default: Default).
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: Project domain name (default: Default).
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret
|
||||
|
||||
@@ -27,6 +27,7 @@ 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.openstack.openstack_provider import OpenstackProvider
|
||||
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
|
||||
|
||||
|
||||
@@ -120,6 +121,7 @@ class TestReturnProwlerProvider:
|
||||
(Provider.ProviderChoices.IAC.value, IacProvider),
|
||||
(Provider.ProviderChoices.ALIBABACLOUD.value, AlibabacloudProvider),
|
||||
(Provider.ProviderChoices.CLOUDFLARE.value, CloudflareProvider),
|
||||
(Provider.ProviderChoices.OPENSTACK.value, OpenstackProvider),
|
||||
],
|
||||
)
|
||||
def test_return_prowler_provider(self, provider_type, expected_provider):
|
||||
@@ -227,6 +229,10 @@ class TestGetProwlerProviderKwargs:
|
||||
Provider.ProviderChoices.CLOUDFLARE.value,
|
||||
{"filter_accounts": ["provider_uid"]},
|
||||
),
|
||||
(
|
||||
Provider.ProviderChoices.OPENSTACK.value,
|
||||
{},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
|
||||
|
||||
@@ -1179,6 +1179,11 @@ class TestProviderViewSet:
|
||||
"uid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
|
||||
"alias": "Cloudflare Account",
|
||||
},
|
||||
{
|
||||
"provider": "openstack",
|
||||
"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"alias": "OpenStack Project",
|
||||
},
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -1598,6 +1603,26 @@ class TestProviderViewSet:
|
||||
"cloudflare-uid",
|
||||
"uid",
|
||||
),
|
||||
# OpenStack UID validation - starts with special character
|
||||
(
|
||||
{
|
||||
"provider": "openstack",
|
||||
"uid": "-invalid-project",
|
||||
"alias": "test",
|
||||
},
|
||||
"openstack-uid",
|
||||
"uid",
|
||||
),
|
||||
# OpenStack UID validation - too short (below min_length)
|
||||
(
|
||||
{
|
||||
"provider": "openstack",
|
||||
"uid": "ab",
|
||||
"alias": "test",
|
||||
},
|
||||
"min_length",
|
||||
"uid",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -1771,21 +1796,21 @@ class TestProviderViewSet:
|
||||
(
|
||||
"uid.icontains",
|
||||
"1",
|
||||
9,
|
||||
10,
|
||||
),
|
||||
("alias", "aws_testing_1", 1),
|
||||
("alias.icontains", "aws", 2),
|
||||
("inserted_at", TODAY, 10),
|
||||
("inserted_at", TODAY, 11),
|
||||
(
|
||||
"inserted_at.gte",
|
||||
"2024-01-01",
|
||||
10,
|
||||
11,
|
||||
),
|
||||
("inserted_at.lte", "2024-01-01", 0),
|
||||
(
|
||||
"updated_at.gte",
|
||||
"2024-01-01",
|
||||
10,
|
||||
11,
|
||||
),
|
||||
("updated_at.lte", "2024-01-01", 0),
|
||||
]
|
||||
@@ -2392,6 +2417,15 @@ class TestProviderSecretViewSet:
|
||||
"api_email": "user@example.com",
|
||||
},
|
||||
),
|
||||
# OpenStack with clouds.yaml content
|
||||
(
|
||||
Provider.ProviderChoices.OPENSTACK.value,
|
||||
ProviderSecret.TypeChoices.STATIC,
|
||||
{
|
||||
"clouds_yaml_content": "clouds:\n mycloud:\n auth:\n auth_url: https://openstack.example.com:5000/v3\n",
|
||||
"clouds_yaml_cloud": "mycloud",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_provider_secrets_create_valid(
|
||||
|
||||
@@ -33,6 +33,7 @@ if TYPE_CHECKING:
|
||||
from prowler.providers.mongodbatlas.mongodbatlas_provider import (
|
||||
MongodbatlasProvider,
|
||||
)
|
||||
from prowler.providers.openstack.openstack_provider import OpenstackProvider
|
||||
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
|
||||
|
||||
|
||||
@@ -78,12 +79,14 @@ def return_prowler_provider(
|
||||
AlibabacloudProvider
|
||||
| AwsProvider
|
||||
| AzureProvider
|
||||
| CloudflareProvider
|
||||
| GcpProvider
|
||||
| GithubProvider
|
||||
| IacProvider
|
||||
| KubernetesProvider
|
||||
| M365Provider
|
||||
| MongodbatlasProvider
|
||||
| OpenstackProvider
|
||||
| OraclecloudProvider
|
||||
):
|
||||
"""Return the Prowler provider class based on the given provider type.
|
||||
@@ -92,7 +95,7 @@ def return_prowler_provider(
|
||||
provider (Provider): The provider object containing the provider type and associated secrets.
|
||||
|
||||
Returns:
|
||||
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: The corresponding provider class.
|
||||
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: The corresponding provider class.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider type specified in `provider.provider` is not supported.
|
||||
@@ -152,6 +155,10 @@ def return_prowler_provider(
|
||||
)
|
||||
|
||||
prowler_provider = CloudflareProvider
|
||||
case Provider.ProviderChoices.OPENSTACK.value:
|
||||
from prowler.providers.openstack.openstack_provider import OpenstackProvider
|
||||
|
||||
prowler_provider = OpenstackProvider
|
||||
case _:
|
||||
raise ValueError(f"Provider type {provider.provider} not supported")
|
||||
return prowler_provider
|
||||
@@ -208,6 +215,12 @@ def get_prowler_provider_kwargs(
|
||||
**prowler_provider_kwargs,
|
||||
"filter_accounts": [provider.uid],
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.OPENSTACK.value:
|
||||
# No extra kwargs needed: clouds_yaml_content and clouds_yaml_cloud from the
|
||||
# secret are sufficient. Validating project_id (provider.uid) against the
|
||||
# clouds.yaml is not feasible because not all auth methods include it and the
|
||||
# Keystone API is unavailable on public clouds.
|
||||
pass
|
||||
|
||||
if mutelist_processor:
|
||||
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
|
||||
@@ -232,6 +245,7 @@ def initialize_prowler_provider(
|
||||
| KubernetesProvider
|
||||
| M365Provider
|
||||
| MongodbatlasProvider
|
||||
| OpenstackProvider
|
||||
| OraclecloudProvider
|
||||
):
|
||||
"""Initialize a Prowler provider instance based on the given provider type.
|
||||
@@ -241,7 +255,7 @@ def initialize_prowler_provider(
|
||||
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
|
||||
|
||||
Returns:
|
||||
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: An instance of the corresponding provider class
|
||||
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: An instance of the corresponding provider class
|
||||
initialized with the provider's secrets.
|
||||
"""
|
||||
prowler_provider = return_prowler_provider(provider)
|
||||
@@ -276,6 +290,13 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
|
||||
if "access_token" in prowler_provider_kwargs:
|
||||
iac_test_kwargs["access_token"] = prowler_provider_kwargs["access_token"]
|
||||
return prowler_provider.test_connection(**iac_test_kwargs)
|
||||
elif provider.provider == Provider.ProviderChoices.OPENSTACK.value:
|
||||
openstack_kwargs = {
|
||||
"clouds_yaml_content": prowler_provider_kwargs["clouds_yaml_content"],
|
||||
"clouds_yaml_cloud": prowler_provider_kwargs["clouds_yaml_cloud"],
|
||||
"raise_on_exception": False,
|
||||
}
|
||||
return prowler_provider.test_connection(**openstack_kwargs)
|
||||
else:
|
||||
return prowler_provider.test_connection(
|
||||
**prowler_provider_kwargs,
|
||||
|
||||
@@ -373,6 +373,21 @@ from rest_framework_json_api import serializers
|
||||
},
|
||||
"required": ["api_key", "api_email"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "OpenStack clouds.yaml Credentials",
|
||||
"properties": {
|
||||
"clouds_yaml_content": {
|
||||
"type": "string",
|
||||
"description": "The full content of a clouds.yaml configuration file.",
|
||||
},
|
||||
"clouds_yaml_cloud": {
|
||||
"type": "string",
|
||||
"description": "The name of the cloud to use from the clouds.yaml file.",
|
||||
},
|
||||
},
|
||||
"required": ["clouds_yaml_content", "clouds_yaml_cloud"],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1525,6 +1525,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
|
||||
"or both 'api_key' and 'api_email'."
|
||||
}
|
||||
)
|
||||
elif provider_type == Provider.ProviderChoices.OPENSTACK.value:
|
||||
serializer = OpenStackCloudsYamlProviderSecret(data=secret)
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
{"provider": f"Provider type not supported {provider_type}"}
|
||||
@@ -1691,6 +1693,14 @@ class CloudflareApiKeyProviderSecret(serializers.Serializer):
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class OpenStackCloudsYamlProviderSecret(serializers.Serializer):
|
||||
clouds_yaml_content = serializers.CharField()
|
||||
clouds_yaml_cloud = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class AlibabaCloudProviderSecret(serializers.Serializer):
|
||||
access_key_id = serializers.CharField()
|
||||
access_key_secret = serializers.CharField()
|
||||
|
||||
@@ -18,6 +18,10 @@ DATABASES = {
|
||||
|
||||
DATABASE_ROUTERS = []
|
||||
TESTING = True
|
||||
# Override page size for testing to a value only slightly above the current fixture count.
|
||||
# We explicitly set PAGE_SIZE to 15 (round number just above fixture) to avoid masking pagination bugs, while not setting it excessively high.
|
||||
# If you add more providers to the fixture, please review that the total value is below the current one and update this value if needed.
|
||||
REST_FRAMEWORK["PAGE_SIZE"] = 15 # noqa: F405
|
||||
SECRETS_ENCRYPTION_KEY = "ZMiYVo7m4Fbe2eXXPyrwxdJss2WSalXSv3xHBcJkPl0="
|
||||
|
||||
# DRF Simple API Key settings
|
||||
|
||||
@@ -537,6 +537,12 @@ def providers_fixture(tenants_fixture):
|
||||
alias="cloudflare_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider11 = Provider.objects.create(
|
||||
provider="openstack",
|
||||
uid="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
alias="openstack_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
return (
|
||||
provider1,
|
||||
@@ -549,6 +555,7 @@ def providers_fixture(tenants_fixture):
|
||||
provider8,
|
||||
provider9,
|
||||
provider10,
|
||||
provider11,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
|
||||
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
|
||||
@@ -90,6 +93,7 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS),
|
||||
(lambda name: name == "ccc_aws", CCC_AWS),
|
||||
(lambda name: name.startswith("c5_"), AWSC5),
|
||||
(lambda name: name.startswith("csa_"), AWSCSA),
|
||||
],
|
||||
"azure": [
|
||||
(lambda name: name.startswith("cis_"), AzureCIS),
|
||||
@@ -99,6 +103,7 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name == "ccc_azure", CCC_Azure),
|
||||
(lambda name: name == "prowler_threatscore_azure", ProwlerThreatScoreAzure),
|
||||
(lambda name: name == "c5_azure", AzureC5),
|
||||
(lambda name: name.startswith("csa_"), AzureCSA),
|
||||
],
|
||||
"gcp": [
|
||||
(lambda name: name.startswith("cis_"), GCPCIS),
|
||||
@@ -108,6 +113,7 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name == "prowler_threatscore_gcp", ProwlerThreatScoreGCP),
|
||||
(lambda name: name == "ccc_gcp", CCC_GCP),
|
||||
(lambda name: name == "c5_gcp", GCPC5),
|
||||
(lambda name: name.startswith("csa_"), GCPCSA),
|
||||
],
|
||||
"kubernetes": [
|
||||
(lambda name: name.startswith("cis_"), KubernetesCIS),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_kisa_ismsp
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_ID"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_kisa_ismsp(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_kisa_ismsp
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_ID"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_kisa_ismsp(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_kisa_ismsp
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_ID"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_kisa_ismsp(
|
||||
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
|
||||
)
|
||||
@@ -175,15 +175,15 @@ The timeout accepts values in seconds (`s`), minutes (`m`), or hours (`h`). Defa
|
||||
|
||||
### Authentication for Private Registries
|
||||
|
||||
To scan images from private registries, the Image provider supports three authentication methods. Prowler evaluates them in the following order of priority:
|
||||
To scan images from private registries, the Image provider supports three authentication methods. Prowler uses the first available method in this priority order:
|
||||
|
||||
#### 1. Basic Authentication (Environment Variables)
|
||||
|
||||
To authenticate with a username and password, set the `TRIVY_USERNAME` and `TRIVY_PASSWORD` environment variables. Prowler automatically runs `docker login`, pulls the image, and performs a `docker logout` after the scan completes:
|
||||
To authenticate with a username and password, set the `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables. Prowler automatically runs `docker login`, pulls the image, and performs a `docker logout` after the scan completes:
|
||||
|
||||
```bash
|
||||
export TRIVY_USERNAME="myuser"
|
||||
export TRIVY_PASSWORD="mypassword"
|
||||
export REGISTRY_USERNAME="myuser"
|
||||
export REGISTRY_PASSWORD="mypassword"
|
||||
|
||||
prowler image -I myregistry.io/myapp:v1.0
|
||||
```
|
||||
@@ -192,10 +192,10 @@ Both variables must be set for this method to activate. Prowler handles the full
|
||||
|
||||
#### 2. Token-Based Authentication
|
||||
|
||||
To authenticate using a registry token (such as a bearer or OAuth2 token), set the `TRIVY_REGISTRY_TOKEN` environment variable. Prowler passes the token directly to Trivy:
|
||||
To authenticate using a registry token (such as a bearer or OAuth2 token), set the `REGISTRY_TOKEN` environment variable. Prowler passes the token directly to Trivy:
|
||||
|
||||
```bash
|
||||
export TRIVY_REGISTRY_TOKEN="my-registry-token"
|
||||
export REGISTRY_TOKEN="my-registry-token"
|
||||
|
||||
prowler image -I myregistry.io/myapp:v1.0
|
||||
```
|
||||
@@ -220,7 +220,7 @@ When basic authentication is active (method 1), Prowler automatically logs out f
|
||||
|
||||
The Image provider categorizes common Trivy errors with actionable guidance:
|
||||
|
||||
* **Authentication failure (401/403):** Registry credentials are missing or invalid. Verify the `TRIVY_USERNAME`/`TRIVY_PASSWORD` or `TRIVY_REGISTRY_TOKEN` environment variables, or run `docker login` for the target registry and retry the scan.
|
||||
* **Authentication failure (401/403):** Registry credentials are missing or invalid. Verify the `REGISTRY_USERNAME`/`REGISTRY_PASSWORD` or `REGISTRY_TOKEN` environment variables, or run `docker login` for the target registry and retry the scan.
|
||||
* **Image not found (404):** The specified image name, tag, or registry is incorrect. Verify the image reference exists and is accessible.
|
||||
* **Rate limited (429):** The container registry is throttling requests. Wait before retrying, or authenticate to increase rate limits.
|
||||
* **Network issue:** Trivy cannot reach the registry due to connectivity problems. Check network access, DNS resolution, and firewall rules.
|
||||
@@ -6,14 +6,21 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
|
||||
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)
|
||||
- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832)
|
||||
- AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975)
|
||||
- CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
|
||||
- CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
|
||||
- CSA CCM for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
|
||||
- OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020)
|
||||
- `image` provider for container image scanning with Trivy integration [(#9984)](https://github.com/prowler-cloud/prowler/pull/9984)
|
||||
- Private registry authentication for the Image provider via environment variables (`REGISTRY_USERNAME`/`REGISTRY_PASSWORD`, `REGISTRY_TOKEN`) [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Update Azure Monitor service metadata to new format [(#9622)](https://github.com/prowler-cloud/prowler/pull/9622)
|
||||
- Parallelize Cloudflare zone API calls with threading to improve scan performance [(#9982)](https://github.com/prowler-cloud/prowler/pull/9982)
|
||||
|
||||
## [5.18.2] (Prowler UNRELEASED)
|
||||
|
||||
@@ -24,7 +31,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [5.18.0] (Prowler v5.18.0)
|
||||
|
||||
### 🚀 Added
|
||||
@@ -95,7 +101,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- CIS 5.0 compliance framework for the Azure provider [(#9777)](https://github.com/prowler-cloud/prowler/pull/9777)
|
||||
- `Cloudflare` Bot protection, WAF, Privacy, Anti-Scraping and Zone configuration checks [(#9425)](https://github.com/prowler-cloud/prowler/pull/9425)
|
||||
- `Cloudflare` `waf` and `dns record` checks [(#9426)](https://github.com/prowler-cloud/prowler/pull/9426)
|
||||
- Container Image provider using Trivy for vulnerability and secret scanning
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
|
||||
from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
|
||||
@@ -635,6 +638,18 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(c5)
|
||||
c5.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_aws":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_aws = AWSCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_aws)
|
||||
csa_ccm_4_0_aws.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -738,6 +753,18 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(c5_azure)
|
||||
c5_azure.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_azure":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_azure = AzureCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_azure)
|
||||
csa_ccm_4_0_azure.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -841,6 +868,18 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(c5_gcp)
|
||||
c5_gcp.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_gcp":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_gcp = GCPCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_gcp)
|
||||
csa_ccm_4_0_gcp.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -226,6 +226,18 @@ class C5Germany_Requirement_Attribute(BaseModel):
|
||||
ComplementaryCriteria: str
|
||||
|
||||
|
||||
# CSA CCM v4 Requirement Attribute
|
||||
class CSA_CCM_Requirement_Attribute(BaseModel):
|
||||
"""CSA Cloud Controls Matrix (CCM) v4 Requirement Attribute"""
|
||||
|
||||
Section: str
|
||||
CCMLite: str
|
||||
IaaS: str
|
||||
PaaS: str
|
||||
SaaS: str
|
||||
ScopeApplicability: list[dict]
|
||||
|
||||
|
||||
# Base Compliance Model
|
||||
# TODO: move this to compliance folder
|
||||
class Compliance_Requirement(BaseModel):
|
||||
@@ -244,6 +256,7 @@ class Compliance_Requirement(BaseModel):
|
||||
Prowler_ThreatScore_Requirement_Attribute,
|
||||
CCC_Requirement_Attribute,
|
||||
C5Germany_Requirement_Attribute,
|
||||
CSA_CCM_Requirement_Attribute,
|
||||
# Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework
|
||||
Generic_Compliance_Requirement_Attribute,
|
||||
]
|
||||
|
||||
@@ -870,7 +870,7 @@ class CheckReportImage(Check_Report):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
metadata: Optional[str] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
finding: Optional[dict] = None,
|
||||
image_name: str = "",
|
||||
) -> None:
|
||||
@@ -878,7 +878,7 @@ class CheckReportImage(Check_Report):
|
||||
Initialize the Container Image Check's finding information from a Trivy vulnerability/secret dict.
|
||||
|
||||
Args:
|
||||
metadata (str): Check metadata.
|
||||
metadata (Dict): Check metadata.
|
||||
finding (dict): A single vulnerability/secret result from Trivy's JSON output.
|
||||
image_name (str): The container image name being scanned.
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ from prowler.lib.check.models import Check_Report
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.compliance.c5.c5 import get_c5_table
|
||||
from prowler.lib.outputs.compliance.cis.cis import get_cis_table
|
||||
from prowler.lib.outputs.compliance.csa.csa import get_csa_table
|
||||
from prowler.lib.outputs.compliance.ens.ens import get_ens_table
|
||||
from prowler.lib.outputs.compliance.generic.generic_table import (
|
||||
get_generic_compliance_table,
|
||||
@@ -85,6 +86,15 @@ def display_compliance_table(
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
elif "csa_ccm_" in compliance_framework:
|
||||
get_csa_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
elif "c5_" in compliance_framework:
|
||||
get_c5_table(
|
||||
findings,
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,101 @@
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color
|
||||
|
||||
|
||||
def get_csa_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
section_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
"Status": [],
|
||||
"Muted": [],
|
||||
}
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
sections = {}
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
compliance.Framework == "CSA-CCM"
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
|
||||
if section not in sections:
|
||||
sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
sections[section]["PASS"] += 1
|
||||
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
section_table["Provider"].append(compliance.Provider)
|
||||
section_table["Section"].append(section)
|
||||
if sections[section]["FAIL"] > 0:
|
||||
section_table["Status"].append(
|
||||
f"{Fore.RED}FAIL({sections[section]['FAIL']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
if sections[section]["PASS"] > 0:
|
||||
section_table["Status"].append(
|
||||
f"{Fore.GREEN}PASS({sections[section]['PASS']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
section_table["Status"].append(f"{Fore.GREEN}PASS{Style.RESET_ALL}")
|
||||
section_table["Muted"].append(
|
||||
f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
if len(fail_count) > 0 and len(section_table["Section"]) > 0:
|
||||
print(
|
||||
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
section_table,
|
||||
tablefmt="rounded_grid",
|
||||
headers="keys",
|
||||
)
|
||||
)
|
||||
print(f"\nDetailed results of {compliance_framework.upper()} are in:")
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import AWSCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AWSCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the AWS CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into AWS CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into AWS CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AWSCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=finding.account_uid,
|
||||
Region=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AWSCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
AccountId="",
|
||||
Region="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -0,0 +1,96 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import AzureCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AzureCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the Azure CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into Azure CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into Azure CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AzureCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
SubscriptionId=finding.account_uid,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AzureCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
SubscriptionId="",
|
||||
Location="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -0,0 +1,96 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import GCPCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class GCPCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the GCP CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into GCP CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into GCP CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = GCPCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
ProjectId=finding.account_uid,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = GCPCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
ProjectId="",
|
||||
Location="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -0,0 +1,88 @@
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
|
||||
class AWSCSAModel(BaseModel):
|
||||
"""
|
||||
AWSCSAModel generates a finding's output in CSV CSA format for AWS.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
AccountId: str
|
||||
Region: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class GCPCSAModel(BaseModel):
|
||||
"""
|
||||
GCPCSAModel generates a finding's output in CSV CSA format for GCP.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
ProjectId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class AzureCSAModel(BaseModel):
|
||||
"""
|
||||
AzureCSAModel generates a finding's output in CSV CSA format for Azure.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
SubscriptionId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
@@ -1,5 +1,10 @@
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider
|
||||
|
||||
MAX_WORKERS = 10
|
||||
|
||||
|
||||
class CloudflareService:
|
||||
"""Base class for Cloudflare services to share provider context."""
|
||||
@@ -10,3 +15,29 @@ class CloudflareService:
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
self.service = service.lower() if not service.islower() else service
|
||||
|
||||
# Thread pool for __threading_call__
|
||||
self.thread_pool = ThreadPoolExecutor(max_workers=MAX_WORKERS)
|
||||
|
||||
def __threading_call__(self, call, iterator):
|
||||
"""Execute a function across multiple items using threading."""
|
||||
items = list(iterator) if not isinstance(iterator, list) else iterator
|
||||
|
||||
futures = {self.thread_pool.submit(call, item): item for item in items}
|
||||
results = []
|
||||
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
result = future.result()
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
except Exception as error:
|
||||
# Log unhandled exceptions from threaded calls
|
||||
item = futures[future]
|
||||
item_id = getattr(item, "id", str(item))
|
||||
logger.error(
|
||||
f"{self.service} - Threading error processing {item_id}: "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -49,13 +49,13 @@ class Zone(CloudflareService):
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.zones: dict[str, "CloudflareZone"] = {}
|
||||
self._list_zones()
|
||||
self._get_zones_settings()
|
||||
self._get_zones_dnssec()
|
||||
self._get_zones_universal_ssl()
|
||||
self._get_zones_rate_limit_rules()
|
||||
self._get_zones_bot_management()
|
||||
self._get_zones_firewall_rules()
|
||||
self._get_zones_waf_rulesets()
|
||||
self.__threading_call__(self._get_zone_settings_threaded, self.zones.values())
|
||||
self.__threading_call__(self._get_zone_dnssec, self.zones.values())
|
||||
self.__threading_call__(self._get_zone_universal_ssl, self.zones.values())
|
||||
self.__threading_call__(self._get_zone_rate_limit_rules, self.zones.values())
|
||||
self.__threading_call__(self._get_zone_bot_management, self.zones.values())
|
||||
self.__threading_call__(self._get_zone_firewall_rules, self.zones.values())
|
||||
self.__threading_call__(self._get_zone_waf_rulesets, self.zones.values())
|
||||
|
||||
def _list_zones(self) -> None:
|
||||
"""List all Cloudflare zones with their basic information."""
|
||||
@@ -124,110 +124,89 @@ class Zone(CloudflareService):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_settings(self) -> None:
|
||||
"""Get settings for all zones."""
|
||||
logger.info("Zone - Getting zone settings...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
zone.settings = self._get_zone_settings(zone.id)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
def _get_zone_settings_threaded(self, zone: "CloudflareZone") -> None:
|
||||
"""Get settings for a single zone (thread-safe)."""
|
||||
try:
|
||||
zone.settings = self._get_zone_settings(zone.id)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_dnssec(self) -> None:
|
||||
"""Get DNSSEC status for all zones."""
|
||||
logger.info("Zone - Getting DNSSEC status...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
dnssec = self.client.dns.dnssec.get(zone_id=zone.id)
|
||||
zone.dnssec_status = getattr(dnssec, "status", None)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
def _get_zone_dnssec(self, zone: "CloudflareZone") -> None:
|
||||
"""Get DNSSEC status for a single zone."""
|
||||
try:
|
||||
dnssec = self.client.dns.dnssec.get(zone_id=zone.id)
|
||||
zone.dnssec_status = getattr(dnssec, "status", None)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_universal_ssl(self) -> None:
|
||||
"""Get Universal SSL settings for all zones."""
|
||||
logger.info("Zone - Getting Universal SSL settings...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
universal_ssl = self.client.ssl.universal.settings.get(zone_id=zone.id)
|
||||
zone.settings.universal_ssl_enabled = getattr(
|
||||
universal_ssl, "enabled", False
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
def _get_zone_universal_ssl(self, zone: "CloudflareZone") -> None:
|
||||
"""Get Universal SSL settings for a single zone."""
|
||||
try:
|
||||
universal_ssl = self.client.ssl.universal.settings.get(zone_id=zone.id)
|
||||
zone.settings.universal_ssl_enabled = getattr(
|
||||
universal_ssl, "enabled", False
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_rate_limit_rules(self) -> None:
|
||||
"""Get rate limiting rules for all zones."""
|
||||
logger.info("Zone - Getting rate limit rules...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
seen_ruleset_ids: set[str] = set()
|
||||
for ruleset in self.client.rulesets.list(zone_id=zone.id):
|
||||
ruleset_id = getattr(ruleset, "id", "")
|
||||
if ruleset_id in seen_ruleset_ids:
|
||||
break
|
||||
seen_ruleset_ids.add(ruleset_id)
|
||||
def _get_zone_rate_limit_rules(self, zone: "CloudflareZone") -> None:
|
||||
"""Get rate limiting rules for a single zone."""
|
||||
try:
|
||||
seen_ruleset_ids: set[str] = set()
|
||||
for ruleset in self.client.rulesets.list(zone_id=zone.id):
|
||||
ruleset_id = getattr(ruleset, "id", "")
|
||||
if ruleset_id in seen_ruleset_ids:
|
||||
break
|
||||
seen_ruleset_ids.add(ruleset_id)
|
||||
|
||||
phase = getattr(ruleset, "phase", "")
|
||||
if phase == "http_ratelimit":
|
||||
try:
|
||||
ruleset_detail = self.client.rulesets.get(
|
||||
ruleset_id=ruleset_id, zone_id=zone.id
|
||||
)
|
||||
rules = getattr(ruleset_detail, "rules", []) or []
|
||||
seen_rule_ids: set[str] = set()
|
||||
for rule in rules:
|
||||
rule_id = getattr(rule, "id", "")
|
||||
if rule_id in seen_rule_ids:
|
||||
break
|
||||
seen_rule_ids.add(rule_id)
|
||||
zone.rate_limit_rules.append(
|
||||
CloudflareRateLimitRule(
|
||||
id=rule_id,
|
||||
description=getattr(rule, "description", None),
|
||||
action=getattr(rule, "action", None),
|
||||
enabled=getattr(rule, "enabled", True),
|
||||
expression=getattr(rule, "expression", None),
|
||||
)
|
||||
phase = getattr(ruleset, "phase", "")
|
||||
if phase == "http_ratelimit":
|
||||
try:
|
||||
ruleset_detail = self.client.rulesets.get(
|
||||
ruleset_id=ruleset_id, zone_id=zone.id
|
||||
)
|
||||
rules = getattr(ruleset_detail, "rules", []) or []
|
||||
seen_rule_ids: set[str] = set()
|
||||
for rule in rules:
|
||||
rule_id = getattr(rule, "id", "")
|
||||
if rule_id in seen_rule_ids:
|
||||
break
|
||||
seen_rule_ids.add(rule_id)
|
||||
zone.rate_limit_rules.append(
|
||||
CloudflareRateLimitRule(
|
||||
id=rule_id,
|
||||
description=getattr(rule, "description", None),
|
||||
action=getattr(rule, "action", None),
|
||||
enabled=getattr(rule, "enabled", True),
|
||||
expression=getattr(rule, "expression", None),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.debug(
|
||||
f"{zone.id} ruleset {ruleset_id} -- {error.__class__.__name__}: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.debug(
|
||||
f"{zone.id} ruleset {ruleset_id} -- {error.__class__.__name__}: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_bot_management(self) -> None:
|
||||
"""Get Bot Management settings for all zones."""
|
||||
logger.info("Zone - Getting Bot Management settings...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
bot_management = self.client.bot_management.get(zone_id=zone.id)
|
||||
zone.settings.bot_fight_mode_enabled = getattr(
|
||||
bot_management, "fight_mode", False
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_firewall_rules(self) -> None:
|
||||
"""Get firewall rules for all zones."""
|
||||
logger.info("Zone - Getting firewall rules...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
self._get_zone_firewall_rules(zone)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
def _get_zone_bot_management(self, zone: "CloudflareZone") -> None:
|
||||
"""Get Bot Management settings for a single zone."""
|
||||
try:
|
||||
bot_management = self.client.bot_management.get(zone_id=zone.id)
|
||||
zone.settings.bot_fight_mode_enabled = getattr(
|
||||
bot_management, "fight_mode", False
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zone_firewall_rules(self, zone: "CloudflareZone") -> None:
|
||||
"""List firewall rules from custom rulesets for a zone."""
|
||||
@@ -282,17 +261,6 @@ class Zone(CloudflareService):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zones_waf_rulesets(self) -> None:
|
||||
"""Get WAF rulesets for all zones."""
|
||||
logger.info("Zone - Getting WAF rulesets...")
|
||||
for zone in self.zones.values():
|
||||
try:
|
||||
self._get_zone_waf_rulesets(zone)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_zone_waf_rulesets(self, zone: "CloudflareZone") -> None:
|
||||
"""List WAF rulesets for a zone using the rulesets API."""
|
||||
seen_ids: set[str] = set()
|
||||
|
||||
@@ -285,9 +285,6 @@ class Provider(ABC):
|
||||
timeout=arguments.timeout,
|
||||
config_path=arguments.config_file,
|
||||
fixer_config=fixer_config,
|
||||
registry_username=getattr(arguments, "registry_username", None),
|
||||
registry_password=getattr(arguments, "registry_password", None),
|
||||
registry_token=getattr(arguments, "registry_token", None),
|
||||
)
|
||||
elif "mongodbatlas" in provider_class_name.lower():
|
||||
provider_class(
|
||||
@@ -312,6 +309,9 @@ class Provider(ABC):
|
||||
elif "openstack" in provider_class_name.lower():
|
||||
provider_class(
|
||||
clouds_yaml_file=getattr(arguments, "clouds_yaml_file", None),
|
||||
clouds_yaml_content=getattr(
|
||||
arguments, "clouds_yaml_content", None
|
||||
),
|
||||
clouds_yaml_cloud=getattr(arguments, "clouds_yaml_cloud", None),
|
||||
auth_url=getattr(arguments, "os_auth_url", None),
|
||||
identity_api_version=getattr(
|
||||
|
||||
@@ -69,9 +69,9 @@ class ImageProvider(Provider):
|
||||
config_path: str | None = None,
|
||||
config_content: dict | None = None,
|
||||
fixer_config: dict | None = None,
|
||||
registry_username: str = None,
|
||||
registry_password: str = None,
|
||||
registry_token: str = None,
|
||||
registry_username: str | None = None,
|
||||
registry_password: str | None = None,
|
||||
registry_token: str | None = None,
|
||||
):
|
||||
logger.info("Instantiating Image Provider...")
|
||||
|
||||
@@ -90,9 +90,9 @@ class ImageProvider(Provider):
|
||||
self._identity = "prowler"
|
||||
|
||||
# Registry authentication (follows IaC pattern: explicit params, env vars internal)
|
||||
self.registry_username = registry_username or os.environ.get("TRIVY_USERNAME")
|
||||
self.registry_password = registry_password or os.environ.get("TRIVY_PASSWORD")
|
||||
self.registry_token = registry_token or os.environ.get("TRIVY_REGISTRY_TOKEN")
|
||||
self.registry_username = registry_username or os.environ.get("REGISTRY_USERNAME")
|
||||
self.registry_password = registry_password or os.environ.get("REGISTRY_PASSWORD")
|
||||
self.registry_token = registry_token or os.environ.get("REGISTRY_TOKEN")
|
||||
self._logged_in_registries: set = set()
|
||||
|
||||
if self.registry_username and self.registry_password:
|
||||
@@ -173,6 +173,8 @@ class ImageProvider(Provider):
|
||||
file=file_path,
|
||||
message=f"Image list file not found: {file_path}",
|
||||
)
|
||||
except (ImageListFileReadError, ImageListFileNotFoundError):
|
||||
raise
|
||||
except Exception as error:
|
||||
raise ImageListFileReadError(
|
||||
file=file_path,
|
||||
@@ -275,7 +277,10 @@ class ImageProvider(Provider):
|
||||
return None
|
||||
|
||||
def _docker_login(self, registry: str | None) -> None:
|
||||
"""Run docker login --password-stdin for a registry."""
|
||||
"""Run docker login --password-stdin for a registry.
|
||||
|
||||
Password is passed via stdin to prevent exposure in process listings.
|
||||
"""
|
||||
registry_label = registry or "Docker Hub"
|
||||
cmd = [
|
||||
"docker",
|
||||
@@ -633,7 +638,7 @@ class ImageProvider(Provider):
|
||||
"""Build environment variables for Trivy, injecting registry credentials.
|
||||
|
||||
When username+password are provided, we rely on docker login (which stores
|
||||
credentials in Docker's credential store). Setting TRIVY_USERNAME/TRIVY_PASSWORD
|
||||
credentials in Docker's credential store). Setting REGISTRY_USERNAME/REGISTRY_PASSWORD as Trivy env vars
|
||||
would cause Trivy to attempt its own token-exchange auth, which fails for
|
||||
Docker Hub and other registries that require docker login.
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@ from colorama import Fore, Style
|
||||
from openstack import config, connect
|
||||
from openstack import exceptions as openstack_exceptions
|
||||
from openstack.connection import Connection as OpenStackConnection
|
||||
from yaml import YAMLError, safe_load
|
||||
|
||||
from prowler.config.config import (
|
||||
default_config_file_path,
|
||||
@@ -49,6 +50,7 @@ class OpenstackProvider(Provider):
|
||||
def __init__(
|
||||
self,
|
||||
clouds_yaml_file: Optional[str] = None,
|
||||
clouds_yaml_content: Optional[str] = None,
|
||||
clouds_yaml_cloud: Optional[str] = None,
|
||||
auth_url: Optional[str] = None,
|
||||
identity_api_version: Optional[str] = None,
|
||||
@@ -68,6 +70,7 @@ class OpenstackProvider(Provider):
|
||||
|
||||
self._session = self.setup_session(
|
||||
clouds_yaml_file=clouds_yaml_file,
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud=clouds_yaml_cloud,
|
||||
auth_url=auth_url,
|
||||
identity_api_version=identity_api_version,
|
||||
@@ -132,6 +135,7 @@ class OpenstackProvider(Provider):
|
||||
@staticmethod
|
||||
def setup_session(
|
||||
clouds_yaml_file: Optional[str] = None,
|
||||
clouds_yaml_content: Optional[str] = None,
|
||||
clouds_yaml_cloud: Optional[str] = None,
|
||||
auth_url: Optional[str] = None,
|
||||
identity_api_version: Optional[str] = None,
|
||||
@@ -145,10 +149,16 @@ class OpenstackProvider(Provider):
|
||||
"""Collect authentication information from clouds.yaml, explicit parameters, or environment variables.
|
||||
|
||||
Authentication priority:
|
||||
1. clouds.yaml file (if clouds_yaml_file or clouds_yaml_cloud provided)
|
||||
1. clouds.yaml content/file (if clouds_yaml_content, clouds_yaml_file, or clouds_yaml_cloud provided)
|
||||
2. Explicit parameters + environment variable fallback
|
||||
"""
|
||||
# Priority 1: clouds.yaml authentication
|
||||
if clouds_yaml_content:
|
||||
logger.info("Using clouds.yaml content string for authentication")
|
||||
return OpenstackProvider._setup_session_from_clouds_yaml_content(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud=clouds_yaml_cloud,
|
||||
)
|
||||
if clouds_yaml_file or clouds_yaml_cloud:
|
||||
logger.info("Using clouds.yaml configuration for authentication")
|
||||
return OpenstackProvider._setup_session_from_clouds_yaml(
|
||||
@@ -203,6 +213,73 @@ class OpenstackProvider(Provider):
|
||||
project_domain_name=resolved_project_domain,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _setup_session_from_clouds_yaml_content(
|
||||
clouds_yaml_content: str,
|
||||
clouds_yaml_cloud: Optional[str] = None,
|
||||
) -> OpenStackSession:
|
||||
"""Setup session from clouds.yaml content provided as a string.
|
||||
|
||||
Parses the YAML content directly instead of writing to a temporary file,
|
||||
following the same pattern as KubernetesProvider.setup_session().
|
||||
|
||||
Args:
|
||||
clouds_yaml_content: The full YAML content of a clouds.yaml file.
|
||||
clouds_yaml_cloud: Cloud name to use from the clouds.yaml content.
|
||||
|
||||
Returns:
|
||||
OpenStackSession configured from the provided clouds.yaml content.
|
||||
|
||||
Raises:
|
||||
OpenStackInvalidConfigError: If the YAML is malformed or missing required fields.
|
||||
OpenStackCloudNotFoundError: If the specified cloud is not found in the content.
|
||||
"""
|
||||
if not clouds_yaml_cloud:
|
||||
raise OpenStackInvalidConfigError(
|
||||
message="Cloud name (--clouds-yaml-cloud) is required when using clouds.yaml content",
|
||||
)
|
||||
|
||||
try:
|
||||
parsed = safe_load(clouds_yaml_content)
|
||||
except YAMLError as error:
|
||||
raise OpenStackInvalidConfigError(
|
||||
original_exception=error,
|
||||
message=f"Failed to parse clouds.yaml content: {error}",
|
||||
)
|
||||
|
||||
if not isinstance(parsed, dict) or "clouds" not in parsed:
|
||||
raise OpenStackInvalidConfigError(
|
||||
message="Invalid clouds.yaml content: missing 'clouds' key",
|
||||
)
|
||||
|
||||
cloud_config = parsed["clouds"].get(clouds_yaml_cloud)
|
||||
if not cloud_config:
|
||||
raise OpenStackCloudNotFoundError(
|
||||
message=f"Cloud '{clouds_yaml_cloud}' not found in clouds.yaml content",
|
||||
)
|
||||
|
||||
auth_dict = cloud_config.get("auth", {})
|
||||
|
||||
required_fields = ["auth_url", "username", "password"]
|
||||
missing_fields = [
|
||||
field for field in required_fields if not auth_dict.get(field)
|
||||
]
|
||||
if missing_fields:
|
||||
raise OpenStackInvalidConfigError(
|
||||
message=f"Missing required fields in clouds.yaml for cloud '{clouds_yaml_cloud}': {', '.join(missing_fields)}",
|
||||
)
|
||||
|
||||
return OpenStackSession(
|
||||
auth_url=auth_dict.get("auth_url"),
|
||||
identity_api_version=str(cloud_config.get("identity_api_version", "3")),
|
||||
username=auth_dict.get("username"),
|
||||
password=auth_dict.get("password"),
|
||||
project_id=auth_dict.get("project_id") or auth_dict.get("project_name"),
|
||||
region_name=cloud_config.get("region_name"),
|
||||
user_domain_name=auth_dict.get("user_domain_name", "Default"),
|
||||
project_domain_name=auth_dict.get("project_domain_name", "Default"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _setup_session_from_clouds_yaml(
|
||||
clouds_yaml_file: Optional[str] = None,
|
||||
@@ -394,6 +471,7 @@ class OpenstackProvider(Provider):
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
clouds_yaml_file: Optional[str] = None,
|
||||
clouds_yaml_content: Optional[str] = None,
|
||||
clouds_yaml_cloud: Optional[str] = None,
|
||||
auth_url: Optional[str] = None,
|
||||
identity_api_version: Optional[str] = None,
|
||||
@@ -412,6 +490,7 @@ class OpenstackProvider(Provider):
|
||||
|
||||
Args:
|
||||
clouds_yaml_file: Path to clouds.yaml configuration file
|
||||
clouds_yaml_content: The full content of a clouds.yaml file as a string
|
||||
clouds_yaml_cloud: Cloud name from clouds.yaml to use
|
||||
auth_url: OpenStack Keystone authentication URL
|
||||
identity_api_version: Keystone API version (default: "3")
|
||||
@@ -456,6 +535,7 @@ class OpenstackProvider(Provider):
|
||||
# Setup session with provided credentials
|
||||
session = OpenstackProvider.setup_session(
|
||||
clouds_yaml_file=clouds_yaml_file,
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud=clouds_yaml_cloud,
|
||||
auth_url=auth_url,
|
||||
identity_api_version=identity_api_version,
|
||||
|
||||
@@ -445,7 +445,7 @@ class TestImageProviderRegistryAuth:
|
||||
|
||||
assert provider.auth_method == "Docker login"
|
||||
|
||||
@patch.dict(os.environ, {"TRIVY_USERNAME": "envuser", "TRIVY_PASSWORD": "envpass"})
|
||||
@patch.dict(os.environ, {"REGISTRY_USERNAME": "envuser", "REGISTRY_PASSWORD": "envpass"})
|
||||
def test_basic_auth_from_env_vars(self):
|
||||
"""Test that env vars are used as fallback for basic auth."""
|
||||
provider = _make_provider()
|
||||
@@ -454,7 +454,7 @@ class TestImageProviderRegistryAuth:
|
||||
assert provider.registry_password == "envpass"
|
||||
assert provider.auth_method == "Docker login"
|
||||
|
||||
@patch.dict(os.environ, {"TRIVY_REGISTRY_TOKEN": "env-token"})
|
||||
@patch.dict(os.environ, {"REGISTRY_TOKEN": "env-token"})
|
||||
def test_token_auth_from_env_var(self):
|
||||
"""Test that env var is used as fallback for token auth."""
|
||||
provider = _make_provider()
|
||||
@@ -462,7 +462,7 @@ class TestImageProviderRegistryAuth:
|
||||
assert provider.registry_token == "env-token"
|
||||
assert provider.auth_method == "Registry token"
|
||||
|
||||
@patch.dict(os.environ, {"TRIVY_USERNAME": "envuser", "TRIVY_PASSWORD": "envpass"})
|
||||
@patch.dict(os.environ, {"REGISTRY_USERNAME": "envuser", "REGISTRY_PASSWORD": "envpass"})
|
||||
def test_explicit_params_override_env_vars(self):
|
||||
"""Test that explicit params take precedence over env vars."""
|
||||
provider = _make_provider(
|
||||
|
||||
@@ -7,6 +7,11 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🔄 Changed
|
||||
|
||||
- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
|
||||
- CSA CCM detailed view and small fix related with `Top Failed Sections` width [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- npm dependencies updated to resolve 11 Dependabot alerts (4 HIGH, 7 MEDIUM): fast-xml-parser, @modelcontextprotocol/sdk, tar, @isaacs/brace-expansion, hono, lodash, lodash-es [(#10052)](https://github.com/prowler-cloud/prowler/pull/10052)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
export const getFindings = async ({
|
||||
page = 1,
|
||||
@@ -24,12 +25,7 @@ export const getFindings = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
// Skip filter[search] since it's already added via the `query` param above
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const findings = await fetch(url.toString(), {
|
||||
@@ -65,12 +61,7 @@ export const getLatestFindings = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
// Skip filter[search] since it's already added via the `query` param above
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const findings = await fetch(url.toString(), {
|
||||
@@ -96,20 +87,13 @@ export const getMetadataInfo = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
// Define filters to exclude
|
||||
const excludedFilters = [
|
||||
appendSanitizedProviderTypeFilters(url, filters, {
|
||||
excludedKeyIncludes: [
|
||||
"region__in",
|
||||
"service__in",
|
||||
"resource_type__in",
|
||||
"resource_groups__in",
|
||||
];
|
||||
if (
|
||||
key !== "filter[search]" &&
|
||||
!excludedFilters.some((filter) => key.includes(filter))
|
||||
) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -136,20 +120,13 @@ export const getLatestMetadataInfo = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
// Define filters to exclude
|
||||
const excludedFilters = [
|
||||
appendSanitizedProviderTypeFilters(url, filters, {
|
||||
excludedKeyIncludes: [
|
||||
"region__in",
|
||||
"service__in",
|
||||
"resource_type__in",
|
||||
"resource_groups__in",
|
||||
];
|
||||
if (
|
||||
key !== "filter[search]" &&
|
||||
!excludedFilters.some((filter) => key.includes(filter))
|
||||
) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { AttackSurfaceOverviewResponse } from "./types";
|
||||
@@ -14,12 +15,7 @@ export const getAttackSurfaceOverview = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
|
||||
@@ -15,11 +16,7 @@ export const getComplianceWatchlist = async ({
|
||||
|
||||
// Append filter parameters (provider_id, provider_type, etc.)
|
||||
// Exclude filter[search] as this endpoint doesn't support text search
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { FindingsSeverityOverviewResponse } from "./types";
|
||||
@@ -28,17 +29,8 @@ export const getFindingsByStatus = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
// Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
// The overviews/findings endpoint does not support status or muted filters
|
||||
// (allowed filters include date, region, provider fields). Exclude unsupported ones.
|
||||
if (
|
||||
key !== "filter[search]" &&
|
||||
key !== "filter[muted]" &&
|
||||
key !== "filter[status]"
|
||||
) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
appendSanitizedProviderTypeFilters(url, filters, {
|
||||
excludedKeys: ["filter[search]", "filter[muted]", "filter[status]"],
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -62,15 +54,8 @@ export const getFindingsBySeverity = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/findings_severity`);
|
||||
|
||||
// Handle multiple filters, but exclude unsupported filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (
|
||||
key !== "filter[search]" &&
|
||||
key !== "filter[muted]" &&
|
||||
value !== undefined
|
||||
) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
appendSanitizedProviderTypeFilters(url, filters, {
|
||||
excludedKeys: ["filter[search]", "filter[muted]"],
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { ProvidersOverviewResponse } from "./types";
|
||||
@@ -28,11 +29,7 @@ export const getProvidersOverview = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { RegionsOverviewResponse } from "./types";
|
||||
@@ -14,12 +15,7 @@ export const getRegionsOverview = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/regions`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { ResourceGroupOverviewResponse } from "./types";
|
||||
@@ -14,12 +15,7 @@ export const getResourceGroupOverview = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/resource-groups`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { CategoryOverviewResponse } from "./types";
|
||||
@@ -14,12 +15,7 @@ export const getCategoryOverview = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/categories`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { ServicesOverviewResponse } from "./types";
|
||||
@@ -14,11 +15,7 @@ export const getServicesOverview = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/services`);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type TimeRange,
|
||||
} from "@/app/(prowler)/_overview/severity-over-time/_constants/time-range.constants";
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { adaptSeverityTrendsResponse } from "./severity-trends.adapter";
|
||||
@@ -27,11 +28,7 @@ const getFindingsSeverityTrends = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/findings_severity/timeseries`);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
export const getThreatScore = async ({
|
||||
@@ -12,12 +13,7 @@ export const getThreatScore = async ({
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/threatscore`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { redirect } from "next/navigation";
|
||||
import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib";
|
||||
import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { appendSanitizedProviderInFilters } from "@/lib/provider-filters";
|
||||
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
|
||||
import { ProvidersApiResponse, ProviderType } from "@/types/providers";
|
||||
|
||||
@@ -15,6 +16,12 @@ export const getProviders = async ({
|
||||
sort = "",
|
||||
filters = {},
|
||||
pageSize = 10,
|
||||
}: {
|
||||
page?: number;
|
||||
query?: string;
|
||||
sort?: string;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
pageSize?: number;
|
||||
}): Promise<ProvidersApiResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
@@ -27,12 +34,7 @@ export const getProviders = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderInFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -60,7 +62,7 @@ export const getAllProviders = async ({
|
||||
}: {
|
||||
query?: string;
|
||||
sort?: string;
|
||||
filters?: Record<string, unknown>;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
} = {}): Promise<ProvidersApiResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const pageSize = 100; // Use larger page size to minimize API calls
|
||||
@@ -79,11 +81,7 @@ export const getAllProviders = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderInFilters(url, filters);
|
||||
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
const data = (await handleApiResponse(response)) as
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
export const getResources = async ({
|
||||
@@ -17,7 +18,7 @@ export const getResources = async ({
|
||||
page?: number;
|
||||
query?: string;
|
||||
sort?: string;
|
||||
filters?: Record<string, string>;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
pageSize?: number;
|
||||
include?: string;
|
||||
fields?: string[];
|
||||
@@ -38,9 +39,7 @@ export const getResources = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, String(value));
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -66,7 +65,7 @@ export const getLatestResources = async ({
|
||||
page?: number;
|
||||
query?: string;
|
||||
sort?: string;
|
||||
filters?: Record<string, string>;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
pageSize?: number;
|
||||
include?: string;
|
||||
fields?: string[];
|
||||
@@ -87,9 +86,7 @@ export const getLatestResources = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, String(value));
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -107,6 +104,10 @@ export const getMetadataInfo = async ({
|
||||
query = "",
|
||||
sort = "",
|
||||
filters = {},
|
||||
}: {
|
||||
query?: string;
|
||||
sort?: string;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
@@ -115,9 +116,7 @@ export const getMetadataInfo = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, String(value));
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -135,6 +134,10 @@ export const getLatestMetadataInfo = async ({
|
||||
query = "",
|
||||
sort = "",
|
||||
filters = {},
|
||||
}: {
|
||||
query?: string;
|
||||
sort?: string;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
@@ -143,9 +146,7 @@ export const getLatestMetadataInfo = async ({
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, String(value));
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
COMPLIANCE_REPORT_DISPLAY_NAMES,
|
||||
type ComplianceReportType,
|
||||
} from "@/lib/compliance/compliance-report-types";
|
||||
import {
|
||||
appendSanitizedProviderTypeFilters,
|
||||
sanitizeProviderTypesCsv,
|
||||
} from "@/lib/provider-filters";
|
||||
import { addScanOperation } from "@/lib/sentry-breadcrumbs";
|
||||
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
|
||||
export const getScans = async ({
|
||||
@@ -35,13 +39,7 @@ export const getScans = async ({
|
||||
url.searchParams.append(`fields[${key}]`, String(value));
|
||||
});
|
||||
|
||||
// Add dynamic filters (e.g., "filter[state]", "fields[scans]")
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
// Skip filter[search] since it's already added via the `query` param above
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
appendSanitizedProviderTypeFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
@@ -58,6 +56,10 @@ export const getScansByState = async () => {
|
||||
const url = new URL(`${apiBaseUrl}/scans`);
|
||||
// Request only the necessary fields to optimize the response
|
||||
url.searchParams.append("fields[scans]", "state");
|
||||
url.searchParams.append(
|
||||
"filter[provider_type__in]",
|
||||
sanitizeProviderTypesCsv(),
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
|
||||
@@ -151,8 +151,9 @@ export default async function ComplianceDetail({
|
||||
{/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 gap-6 md:grid-cols-2",
|
||||
isThreatScore && "xl:grid-cols-[minmax(280px,320px)_auto_1fr]",
|
||||
"grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]",
|
||||
isThreatScore &&
|
||||
"xl:grid-cols-[minmax(280px,320px)_minmax(280px,400px)_1fr]",
|
||||
)}
|
||||
>
|
||||
{isThreatScore && (
|
||||
@@ -209,7 +210,7 @@ const SSRComplianceContent = async ({
|
||||
if (!scanId || type === "tasks") {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]">
|
||||
<RequirementsStatusCard pass={0} fail={0} manual={0} />
|
||||
<TopFailedSectionsCard sections={[]} />
|
||||
{/* <SectionsFailureRateCard categories={[]} /> */}
|
||||
@@ -244,8 +245,9 @@ const SSRComplianceContent = async ({
|
||||
{/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 gap-6 md:grid-cols-2",
|
||||
threatScoreData && "xl:grid-cols-[minmax(280px,320px)_auto_1fr]",
|
||||
"grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]",
|
||||
threatScoreData &&
|
||||
"xl:grid-cols-[minmax(280px,320px)_minmax(280px,400px)_1fr]",
|
||||
)}
|
||||
>
|
||||
{threatScoreData && (
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/providers/table";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { PROVIDER_TYPES, ProviderProps, SearchParamsProps } from "@/types";
|
||||
import { ProviderProps, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Providers({
|
||||
searchParams,
|
||||
@@ -89,22 +89,15 @@ const ProvidersTable = async ({
|
||||
return acc;
|
||||
}, {}) || {};
|
||||
|
||||
// Exclude provider types not yet supported in the UI
|
||||
const enrichedProviders =
|
||||
providersData?.data
|
||||
?.filter((provider: ProviderProps) =>
|
||||
(PROVIDER_TYPES as readonly string[]).includes(
|
||||
provider.attributes.provider,
|
||||
),
|
||||
)
|
||||
.map((provider: ProviderProps) => {
|
||||
const groupNames =
|
||||
provider.relationships?.provider_groups?.data?.map(
|
||||
(group: { id: string }) =>
|
||||
providerGroupDict[group.id] || "Unknown Group",
|
||||
) || [];
|
||||
return { ...provider, groupNames };
|
||||
}) || [];
|
||||
providersData?.data?.map((provider: ProviderProps) => {
|
||||
const groupNames =
|
||||
provider.relationships?.provider_groups?.data?.map(
|
||||
(group: { id: string }) =>
|
||||
providerGroupDict[group.id] || "Unknown Group",
|
||||
) || [];
|
||||
return { ...provider, groupNames };
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { cn } from "@/lib";
|
||||
import { CSA_MAPPING_SECTIONS } from "@/lib/compliance/csa";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
interface CSADetailsProps {
|
||||
requirement: Requirement;
|
||||
}
|
||||
|
||||
export const CSACustomDetails = ({ requirement }: CSADetailsProps) => {
|
||||
const mappingSections = CSA_MAPPING_SECTIONS.map((section) => ({
|
||||
...section,
|
||||
data: requirement[section.key] as Array<{
|
||||
ReferenceId: string;
|
||||
Identifiers: string[];
|
||||
}>,
|
||||
})).filter((section) => section.data && section.data.length > 0);
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.ccm_lite && (
|
||||
<ComplianceBadge
|
||||
label="CCM Lite"
|
||||
value={requirement.ccm_lite as string}
|
||||
color={requirement.ccm_lite === "Yes" ? "green" : "gray"}
|
||||
/>
|
||||
)}
|
||||
{requirement.iaas && (
|
||||
<ComplianceBadge
|
||||
label="IaaS"
|
||||
value={requirement.iaas as string}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
{requirement.paas && (
|
||||
<ComplianceBadge
|
||||
label="PaaS"
|
||||
value={requirement.paas as string}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
{requirement.saas && (
|
||||
<ComplianceBadge
|
||||
label="SaaS"
|
||||
value={requirement.saas as string}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
{mappingSections.map((section) => (
|
||||
<ComplianceDetailSection key={section.title} title={section.title}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{section.data.map((mapping, index) => (
|
||||
<div key={index} className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground text-xs font-medium">
|
||||
{mapping.ReferenceId}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mapping.Identifiers.map((identifier, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
section.colorClasses,
|
||||
)}
|
||||
>
|
||||
{identifier}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
))}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import C5Logo from "./c5.svg";
|
||||
import CCCLogo from "./ccc.svg";
|
||||
import CISLogo from "./cis.svg";
|
||||
import CISALogo from "./cisa.svg";
|
||||
import CSALogo from "./csa.svg";
|
||||
import ENSLogo from "./ens.png";
|
||||
import FedRAMPLogo from "./fedramp.svg";
|
||||
import FFIECLogo from "./ffiec.svg";
|
||||
@@ -40,6 +41,7 @@ const COMPLIANCE_LOGOS = {
|
||||
nis2: NIS2Logo,
|
||||
c5: C5Logo,
|
||||
ccc: CCCLogo,
|
||||
csa: CSALogo,
|
||||
} as const;
|
||||
|
||||
export const getComplianceIcon = (complianceTitle: string) => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_2_00000156551598509933943900000017927917491817594766_" x="0" y="0" style="enable-background:new 0 0 500 178.3" version="1.1" viewBox="0 0 500 178.3"><style>.st0{fill:#00549f}.st1{fill:#f98526}</style><g id="Art"><path d="M26.8 94c-.3 1.2-.5 2.4-.6 3.6-1.9 12.8.9 23 11.4 23 7.7 0 11.5-3.9 17.5-11.9l19 10.4c-7.5 11-18.9 21.7-39.8 21.7C8.2 140.8-3 122.4.7 97.5l.6-3.6c0 .1 25.5.1 25.5.1zM4.1 84.5c7.3-19.1 23.7-30.3 43.2-30.3 21.6 0 28.8 8.9 32.6 24.7l-23.3 8.3c-2-7.8-4.3-12.7-12.3-12.7-6.4 0-11.1 3.8-14.2 10h-26zM144.7 94c6.7 4.4 9.3 10.5 7.9 19.8-2.8 19.2-19.2 27.1-40.3 27.1-14.2 0-27.2-4.4-35.6-10.2l14.5-18.3c5.8 4.2 16.4 8.4 24.5 8.4 8.7 0 11.6-1.4 12.2-5.4.5-3.1-2.4-5-13.8-7.7-13.8-3.2-22.6-6.6-26-13.7h56.6zm-58.4-9.5c.1-1.2.2-2.5.4-3.8 2.3-15.8 17.4-26.5 38.6-26.5 14.9 0 25.2 3.5 33.6 9.7l-14.5 18.2c-7-5-14.4-7.9-23.3-7.9-7.3 0-9.4 2.2-9.8 5.2-.3 2.3 1.1 3.6 6 5.2h-31v-.1zM215.8 84.6l-5.2-29.1h-21.2l-13.8 29.1zM186.1 106.3h12.6l-1.4-12.4h20.2l8.2 45.6H203l-1.9-14.7h-22.7l-6.3 14.7h-22.5L171.2 94h20.1z" class="st0"/><path d="M214.8 17.7c-2.2-1.9-4.3-3-7.7-3-5.3 0-10 4.2-11.2 11.7-1.2 7.3 1.7 12 7.7 12 3.3 0 5.7-1.2 8.3-3.2l2.5 2.7c-3.2 2.6-6.8 4.2-11.6 4.2-7.4 0-13.5-5-11.9-15.4 1.7-11 9.9-15.7 17-15.7 4.6 0 7.8 1.5 10.2 3.5l-3.3 3.2zM222.7 41.4l6-39 5.2-2.4-6.4 41.5-4.8-.1zM249.8 42c-8.3 0-13.7-5.4-12.2-15.5 1.6-10.1 9-15.6 17.3-15.6s13.8 5.2 12.2 15.4c-1.6 10.1-9 15.7-17.3 15.7zm4.4-27.4c-5.9 0-10.4 3.9-11.6 11.8s2.1 11.9 7.9 11.9c5.8 0 10.5-4.1 11.7-11.9 1.3-7.8-2.1-11.8-8-11.8zM293.3 41.4l.6-3.6c-2.3 2.5-5.7 4.3-10 4.3-7.2 0-11.3-3.7-10-12.3l2.8-18.2h4.8L278.9 29c-1 6.4 1.2 9.3 6.6 9.3 4.7 0 8.8-2.8 9.8-9.3l2.7-17.5h4.8l-4.6 29.8h-4.9zM328 41.4l.6-3.6c-2.7 2.3-6.3 4.3-10.8 4.3-6.8 0-11.4-4.5-9.8-15 1.6-10.6 8.2-16 15.3-16 4.3 0 6.9 1.6 8.9 4L334 2.4l5.2-2.4-6.4 41.5-4.8-.1c0 .1 0 0 0 0zm3.4-22.3c-1.5-2.5-4.5-4.5-8.5-4.5-5.6 0-9 4.3-10.3 12.3-1.2 7.5 1.5 11.4 6.8 11.4 3.6 0 6.8-1.7 9.6-4.3l2.4-14.9zM244.8 177.7l.6-3.6c-2.7 2.3-6.4 4.3-11.3 4.3-5.3 0-10.5-2.3-9.4-9.6 1.1-6.7 7.7-9.6 14.5-9.6 3.4 0 6.4.7 8.2 1.7l.5-3.5c.7-4.4-1.4-6.6-6.4-6.6-3.5 0-6.6.8-9.6 2.3l-1.3-3.5c3.2-1.4 7.4-2.4 11.7-2.4 7.3 0 11.5 2.7 10.4 9.7l-3.2 20.8h-4.7zm2-13.4c-2.1-1.1-4.3-1.7-8.5-1.7-4.9 0-8.3 2.1-8.8 5.8-.7 4.4 1.8 6.4 5.9 6.4 4.4 0 8-2 10.4-4.5l1-6zM260.3 177.7l6-39 5.2-2.4-6.4 41.5-4.8-.1zM276.4 177.7l6-39 5.2-2.4-6.4 41.5-4.8-.1zM292.5 177.7l4.6-29.8h4.8l-4.6 29.8h-4.8zm7.9-35.2c-2 0-3.4-1.4-3.1-3 .3-1.7 2.1-3.1 4.1-3.1 1.8 0 3.2 1.4 2.9 3.1-.4 1.6-2.1 3-3.9 3zM327.6 177.7l.6-3.6c-2.7 2.3-6.4 4.3-11.3 4.3-5.3 0-10.5-2.3-9.4-9.6 1.1-6.7 7.7-9.6 14.5-9.6 3.4 0 6.4.7 8.2 1.7l.5-3.5c.7-4.4-1.4-6.6-6.4-6.6-3.5 0-6.6.8-9.6 2.3l-1.3-3.5c3.2-1.4 7.4-2.4 11.7-2.4 7.3 0 11.5 2.7 10.4 9.7l-3.2 20.8h-4.7zm2.1-13.4c-2.1-1.1-4.3-1.7-8.5-1.7-4.9 0-8.3 2.1-8.8 5.8-.7 4.4 1.8 6.4 5.9 6.4 4.4 0 8-2 10.4-4.5l1-6zM364.1 177.7l2.7-17.5c1-6.4-1.2-9.3-6.6-9.3-4.7 0-8.8 2.8-9.8 9.3l-2.7 17.5h-4.8l4.6-29.8h4.8l-.6 3.6c2.3-2.5 5.7-4.3 10-4.3 7.2 0 11.3 3.7 10 12.3l-2.8 18.2h-4.8zM403.6 153.9c-2.2-1.9-4.3-3-7.7-3-5.3 0-10 4.2-11.2 11.6-1.2 7.3 1.7 12 7.7 12 3.3 0 5.7-1.2 8.3-3.2l2.5 2.7c-3.2 2.6-6.8 4.2-11.6 4.2-7.4 0-13.5-5-11.9-15.4 1.7-11 9.9-15.7 17-15.7 4.6 0 7.8 1.5 10.2 3.5l-3.3 3.3zM437.8 164.2h-22.6c-.5 6.4 2.3 10.4 7.8 10.4 3.9 0 6.4-1 9.8-2.8l1.8 3.1c-4.4 2.3-7.6 3.5-12.7 3.5-6.2 0-13.2-4-11.3-15.7 1.5-9.5 8.3-15.4 16.4-15.4 8.8 0 12.7 6.2 11.4 14.7-.2.9-.4 1.5-.6 2.2zm-11.4-13.3c-5 0-9 3.2-10.6 9.8H434c.5-5.8-1.7-9.8-7.6-9.8zM282.4 106.5c-4.6 0-8.5-1.5-11.4-3.8l3.3-3.1c2.4 2.1 5.4 3.3 8.7 3.3 4.6 0 7.1-1.7 7.6-4.6.4-2.4-1.4-3.8-7-6.1-6.4-2.6-8.9-4.4-8.2-9.2.7-4.5 5.6-7.6 11.8-7.6 4.6 0 8 1.3 10.4 3.2l-3.2 3.1c-2.4-1.8-4.4-2.7-8-2.7-3.6 0-5.8 1.5-6.2 3.7-.4 2.5 1.2 3.5 6.6 5.8 6 2.6 9.5 4.2 8.7 9.6-1.1 5.8-7.3 8.4-13.1 8.4zM330.2 92.4h-22.6c-.5 6.4 2.3 10.4 7.8 10.4 3.9 0 6.4-1 9.8-2.8l1.8 3.1c-4.4 2.3-7.6 3.5-12.7 3.5-6.2 0-13.2-4-11.3-15.7 1.5-9.5 8.3-15.4 16.4-15.4 8.8 0 12.7 6.2 11.4 14.7-.2.9-.4 1.5-.6 2.2zm-11.5-13.3c-5 0-9 3.2-10.6 9.8h18.3c.5-5.8-1.7-9.8-7.7-9.8zM361.4 82.1c-2.2-1.9-4.3-3-7.7-3-5.3 0-10 4.2-11.2 11.7-1.2 7.3 1.7 12 7.7 12 3.3 0 5.7-1.2 8.3-3.2l2.5 2.7c-3.2 2.6-6.8 4.2-11.6 4.2-7.4 0-13.5-5-11.9-15.4 1.7-11 9.9-15.7 17-15.7 4.6 0 7.8 1.5 10.2 3.5l-3.3 3.2zM388.8 105.8l.6-3.6c-2.3 2.5-5.7 4.3-10 4.3-7.2 0-11.3-3.7-10-12.3l2.8-18.2h4.8l-2.7 17.5c-1 6.4 1.2 9.3 6.6 9.3 4.7 0 8.8-2.8 9.8-9.3l2.7-17.5h4.8l-4.6 29.8h-4.8zM425.4 80.4c-1.3-.7-2.7-1.2-4.9-1.2-4.7 0-7.9 3.4-8.7 9l-2.7 17.5h-4.8l4.6-29.8h4.8l-.6 3.7c1.9-2.4 5.4-4.3 9-4.3 2.5 0 4.1.3 5.2.7l-1.9 4.4zM430.7 105.8l4.6-29.8h4.8l-4.6 29.8h-4.8zm7.9-35.2c-2 0-3.4-1.4-3.1-3 .3-1.7 2.1-3.1 4.1-3.1 1.8 0 3.2 1.4 2.9 3.1-.3 1.7-2 3-3.9 3zM456.7 79.6l-3 19.5c-.4 2.7.3 3.6 2.9 3.6 1.9 0 3.3-.3 4.9-.8l-.9 3.7c-2 .7-3.8 1-6.4 1-4.2 0-6.2-1.8-5.4-6.8l3.1-20.1h-4.2l.6-3.6h4.2l1.4-9.2 5.2-2.4-1.9 11.5h8.4l-.6 3.6h-8.3zM476.9 116.4h-4.8l6.3-10.8-7.5-29.6h5.1l4.9 21.7.7 3.1 1.6-3L494.9 76h5.1l-23.1 40.4zM4Line truncated
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
+11
-11
@@ -10,10 +10,10 @@
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@aws-sdk/client-bedrock-runtime",
|
||||
"from": "3.943.0",
|
||||
"to": "3.948.0",
|
||||
"from": "3.948.0",
|
||||
"to": "3.988.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-15T08:24:46.195Z"
|
||||
"generatedAt": "2026-02-12T11:04:39.164Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -67,9 +67,9 @@
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/mcp-adapters",
|
||||
"from": "1.0.3",
|
||||
"to": "1.0.3",
|
||||
"to": "1.1.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-12T10:01:54.132Z"
|
||||
"generatedAt": "2026-02-12T11:04:39.164Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -283,9 +283,9 @@
|
||||
"section": "dependencies",
|
||||
"name": "@tailwindcss/postcss",
|
||||
"from": "4.1.13",
|
||||
"to": "4.1.13",
|
||||
"to": "4.1.18",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
"generatedAt": "2026-02-12T11:04:39.164Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -947,9 +947,9 @@
|
||||
"section": "devDependencies",
|
||||
"name": "shadcn",
|
||||
"from": "3.4.1",
|
||||
"to": "3.4.1",
|
||||
"to": "3.8.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T15:52:15.849Z"
|
||||
"generatedAt": "2026-02-12T11:04:39.164Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -963,9 +963,9 @@
|
||||
"section": "devDependencies",
|
||||
"name": "tailwindcss",
|
||||
"from": "4.1.13",
|
||||
"to": "4.1.13",
|
||||
"to": "4.1.18",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
"generatedAt": "2026-02-12T11:04:39.164Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AWSWellArchitectedCustomDetails } from "@/components/compliance/complia
|
||||
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
|
||||
import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details";
|
||||
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
|
||||
import { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details";
|
||||
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
|
||||
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
|
||||
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
|
||||
@@ -37,6 +38,10 @@ import {
|
||||
toAccordionItems as toCISAccordionItems,
|
||||
} from "./cis";
|
||||
import { calculateCategoryHeatmapData, getTopFailedSections } from "./commons";
|
||||
import {
|
||||
mapComplianceData as mapCSAComplianceData,
|
||||
toAccordionItems as toCSAAccordionItems,
|
||||
} from "./csa";
|
||||
import {
|
||||
mapComplianceData as mapENSComplianceData,
|
||||
toAccordionItems as toENSAccordionItems,
|
||||
@@ -179,6 +184,15 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(CCCCustomDetails, { requirement }),
|
||||
},
|
||||
"CSA-CCM": {
|
||||
mapComplianceData: mapCSAComplianceData,
|
||||
toAccordionItems: toCSAAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(CSACustomDetails, { requirement }),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
CSAAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
calculateFrameworkCounters,
|
||||
createRequirementsMap,
|
||||
findOrCreateCategory,
|
||||
findOrCreateControl,
|
||||
findOrCreateFramework,
|
||||
} from "./commons";
|
||||
|
||||
export interface CSAMappingSection {
|
||||
title: string;
|
||||
key: keyof Requirement;
|
||||
colorClasses: string;
|
||||
}
|
||||
|
||||
export const CSA_MAPPING_SECTIONS: CSAMappingSection[] = [
|
||||
{
|
||||
title: "Scope Applicability",
|
||||
key: "scope_applicability",
|
||||
colorClasses:
|
||||
"bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusCounters = (status: RequirementStatus) => ({
|
||||
pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
});
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirementsMap = createRequirementsMap(requirementsData);
|
||||
const frameworks: Framework[] = [];
|
||||
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes
|
||||
?.metadata as unknown as CSAAttributesMetadata[];
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!attrs) continue;
|
||||
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
const categoryName = attrs.Section;
|
||||
const requirementName = attributeItem.attributes.name || "";
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
const category = findOrCreateCategory(framework.categories, categoryName);
|
||||
// Use a single control per category to keep a flat 2-level structure
|
||||
const control = findOrCreateControl(category.controls, categoryName);
|
||||
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
const requirement: Requirement = {
|
||||
name: requirementName ? `${id} - ${requirementName}` : id,
|
||||
description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
...getStatusCounters(finalStatus),
|
||||
ccm_lite: attrs.CCMLite,
|
||||
iaas: attrs.IaaS,
|
||||
paas: attrs.PaaS,
|
||||
saas: attrs.SaaS,
|
||||
scope_applicability: attrs.ScopeApplicability,
|
||||
};
|
||||
|
||||
control.requirements.push(requirement);
|
||||
}
|
||||
|
||||
calculateFrameworkCounters(frameworks);
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
const safeId = scanId || "";
|
||||
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => ({
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
// Flatten: requirements are direct children of the section
|
||||
items: category.controls.flatMap((control) =>
|
||||
control.requirements.map((requirement, reqIndex) => ({
|
||||
key: `${framework.name}-${category.name}-req-${reqIndex}`,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${framework.name}-${category.name}-req-${reqIndex}`}
|
||||
requirement={requirement}
|
||||
scanId={safeId}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
})),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { PROVIDER_TYPES, type ProviderType } from "@/types/providers";
|
||||
|
||||
export type ProviderFilterValue = string | string[] | undefined;
|
||||
export type ProviderFilters = Record<string, ProviderFilterValue>;
|
||||
|
||||
interface AppendProviderFiltersOptions {
|
||||
ensuredInFilterKey?: string;
|
||||
excludedKeys?: string[];
|
||||
excludedKeyIncludes?: string[];
|
||||
}
|
||||
|
||||
type AppendSanitizedProviderTypeFiltersOptions = Omit<
|
||||
AppendProviderFiltersOptions,
|
||||
"ensuredInFilterKey"
|
||||
>;
|
||||
|
||||
const SUPPORTED_PROVIDER_TYPES_CSV = PROVIDER_TYPES.join(",");
|
||||
export const PROVIDER_IN_FILTER_KEY = "filter[provider__in]";
|
||||
export const PROVIDER_TYPE_IN_FILTER_KEY = "filter[provider_type__in]";
|
||||
|
||||
const PROVIDER_TYPE_IN_KEYS = new Set([
|
||||
PROVIDER_TYPE_IN_FILTER_KEY,
|
||||
"provider_type__in",
|
||||
]);
|
||||
|
||||
const PROVIDER_SINGLE_KEYS = new Set([
|
||||
"filter[provider_type]",
|
||||
"provider_type",
|
||||
]);
|
||||
|
||||
const toCsvString = (value: unknown): string => {
|
||||
if (Array.isArray(value)) return value.join(",");
|
||||
if (typeof value === "string") return value;
|
||||
return "";
|
||||
};
|
||||
|
||||
const isSupportedProviderType = (value: string): value is ProviderType =>
|
||||
(PROVIDER_TYPES as readonly string[]).includes(value);
|
||||
|
||||
export const sanitizeProviderTypesCsv = (value?: unknown): string => {
|
||||
const rawValue = toCsvString(value);
|
||||
if (!rawValue.trim()) return SUPPORTED_PROVIDER_TYPES_CSV;
|
||||
|
||||
const supportedProviderTypes = Array.from(
|
||||
new Set(
|
||||
rawValue
|
||||
.split(",")
|
||||
.map((providerType) => providerType.trim())
|
||||
.filter(isSupportedProviderType),
|
||||
),
|
||||
);
|
||||
|
||||
return supportedProviderTypes.length > 0
|
||||
? supportedProviderTypes.join(",")
|
||||
: SUPPORTED_PROVIDER_TYPES_CSV;
|
||||
};
|
||||
|
||||
export const sanitizeProviderType = (
|
||||
value?: unknown,
|
||||
): ProviderType | undefined => {
|
||||
const rawValue = toCsvString(value);
|
||||
if (!rawValue.trim()) return undefined;
|
||||
|
||||
return rawValue
|
||||
.split(",")
|
||||
.map((providerType) => providerType.trim())
|
||||
.find(isSupportedProviderType);
|
||||
};
|
||||
|
||||
export const sanitizeProviderFilters = (
|
||||
filters: ProviderFilters = {},
|
||||
ensuredInFilterKey?: string,
|
||||
): ProviderFilters => {
|
||||
const sanitizedFilters: ProviderFilters = { ...filters };
|
||||
|
||||
Object.keys(sanitizedFilters).forEach((key) => {
|
||||
if (PROVIDER_TYPE_IN_KEYS.has(key)) {
|
||||
sanitizedFilters[key] = sanitizeProviderTypesCsv(sanitizedFilters[key]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PROVIDER_SINGLE_KEYS.has(key)) {
|
||||
const providerType = sanitizeProviderType(sanitizedFilters[key]);
|
||||
if (providerType) {
|
||||
sanitizedFilters[key] = providerType;
|
||||
} else {
|
||||
delete sanitizedFilters[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (ensuredInFilterKey) {
|
||||
sanitizedFilters[ensuredInFilterKey] = sanitizeProviderTypesCsv(
|
||||
sanitizedFilters[ensuredInFilterKey],
|
||||
);
|
||||
}
|
||||
|
||||
return sanitizedFilters;
|
||||
};
|
||||
|
||||
export const appendSanitizedProviderFilters = (
|
||||
url: URL,
|
||||
filters: ProviderFilters = {},
|
||||
{
|
||||
ensuredInFilterKey,
|
||||
excludedKeys = ["filter[search]"],
|
||||
excludedKeyIncludes = [],
|
||||
}: AppendProviderFiltersOptions = {},
|
||||
): void => {
|
||||
const sanitizedFilters = sanitizeProviderFilters(filters, ensuredInFilterKey);
|
||||
const excludedKeysSet = new Set(excludedKeys);
|
||||
|
||||
Object.entries(sanitizedFilters).forEach(([key, value]) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
excludedKeysSet.has(key) ||
|
||||
excludedKeyIncludes.some((excludedKey) => key.includes(excludedKey))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
url.searchParams.append(key, String(value));
|
||||
});
|
||||
};
|
||||
|
||||
export const appendSanitizedProviderTypeFilters = (
|
||||
url: URL,
|
||||
filters: ProviderFilters = {},
|
||||
options: AppendSanitizedProviderTypeFiltersOptions = {},
|
||||
): void =>
|
||||
appendSanitizedProviderFilters(url, filters, {
|
||||
...options,
|
||||
ensuredInFilterKey: PROVIDER_TYPE_IN_FILTER_KEY,
|
||||
});
|
||||
|
||||
export const appendSanitizedProviderInFilters = (
|
||||
url: URL,
|
||||
filters: ProviderFilters = {},
|
||||
options: AppendSanitizedProviderTypeFiltersOptions = {},
|
||||
): void =>
|
||||
appendSanitizedProviderFilters(url, filters, {
|
||||
...options,
|
||||
ensuredInFilterKey: PROVIDER_IN_FILTER_KEY,
|
||||
});
|
||||
+11
-6
@@ -25,14 +25,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "2.0.111",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.948.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.988.0",
|
||||
"@extractus/feed-extractor": "7.1.7",
|
||||
"@heroui/react": "2.8.4",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@internationalized/date": "3.10.0",
|
||||
"@langchain/aws": "1.1.0",
|
||||
"@langchain/core": "1.1.15",
|
||||
"@langchain/mcp-adapters": "1.0.3",
|
||||
"@langchain/mcp-adapters": "1.1.3",
|
||||
"@langchain/openai": "1.1.3",
|
||||
"@next/third-parties": "16.1.6",
|
||||
"@radix-ui/react-alert-dialog": "1.1.14",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@react-types/datepicker": "3.13.2",
|
||||
"@react-types/shared": "3.26.0",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@tailwindcss/postcss": "4.1.13",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@types/dagre": "0.7.53",
|
||||
@@ -144,9 +144,9 @@
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-tailwindcss": "0.6.14",
|
||||
"shadcn": "3.4.1",
|
||||
"shadcn": "3.8.4",
|
||||
"tailwind-variants": "0.1.20",
|
||||
"tailwindcss": "4.1.13",
|
||||
"tailwindcss": "4.1.18",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"pnpm": {
|
||||
@@ -158,7 +158,12 @@
|
||||
"@react-aria/ssr>react": "19.2.4",
|
||||
"@react-aria/ssr>react-dom": "19.2.4",
|
||||
"@react-aria/visually-hidden>react": "19.2.4",
|
||||
"@react-aria/interactions>react": "19.2.4"
|
||||
"@react-aria/interactions>react": "19.2.4",
|
||||
"lodash": "4.17.23",
|
||||
"lodash-es": "4.17.23",
|
||||
"hono": "4.11.7",
|
||||
"@isaacs/brace-expansion": "5.0.1",
|
||||
"fast-xml-parser": "5.3.4"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1",
|
||||
|
||||
Generated
+1012
-806
File diff suppressed because it is too large.
Load diff
@@ -189,6 +189,18 @@ export interface GenericAttributesMetadata {
|
||||
Type: string | null;
|
||||
}
|
||||
|
||||
export interface CSAAttributesMetadata {
|
||||
Section: string;
|
||||
CCMLite: string;
|
||||
IaaS: string;
|
||||
PaaS: string;
|
||||
SaaS: string;
|
||||
ScopeApplicability: Array<{
|
||||
ReferenceId: string;
|
||||
Identifiers: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CCCAttributesMetadata {
|
||||
FamilyName: string;
|
||||
FamilyDescription: string;
|
||||
@@ -227,6 +239,7 @@ export interface AttributesItemData {
|
||||
| C5AttributesMetadata[]
|
||||
| MITREAttributesMetadata[]
|
||||
| CCCAttributesMetadata[]
|
||||
| CSAAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
// MITRE structure
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import oci
|
||||
|
||||
# Logging config
|
||||
logging.basicConfig(
|
||||
stream=sys.stdout,
|
||||
format="%(asctime)s [File: %(filename)s:%(lineno)d] \t[Module: %(module)s]\t %(levelname)s: %(message)s",
|
||||
datefmt="%m/%d/%Y %I:%M:%S %p",
|
||||
level=logging.INFO,
|
||||
)
|
||||
|
||||
# OCI regions are fetched dynamically from the Identity API
|
||||
# No hardcoded display names - following AWS regions updater pattern
|
||||
# This ensures full automation without manual maintenance
|
||||
|
||||
|
||||
def setup_oci_client():
|
||||
"""
|
||||
Set up OCI Identity client using credentials from environment variables.
|
||||
|
||||
Returns:
|
||||
oci.identity.IdentityClient: Authenticated OCI Identity client
|
||||
|
||||
Raises:
|
||||
ValueError: If required environment variables are missing
|
||||
Exception: If authentication fails
|
||||
"""
|
||||
logging.info("Setting up OCI client authentication")
|
||||
|
||||
# Validate required environment variables
|
||||
required_vars = [
|
||||
"OCI_CLI_USER",
|
||||
"OCI_CLI_FINGERPRINT",
|
||||
"OCI_CLI_TENANCY",
|
||||
"OCI_CLI_KEY_CONTENT",
|
||||
]
|
||||
|
||||
missing_vars = [var for var in required_vars if not os.getenv(var)]
|
||||
if missing_vars:
|
||||
raise ValueError(
|
||||
f"Missing required environment variables: {', '.join(missing_vars)}"
|
||||
)
|
||||
|
||||
# Get credentials from environment
|
||||
user_ocid = os.getenv("OCI_CLI_USER")
|
||||
fingerprint = os.getenv("OCI_CLI_FINGERPRINT")
|
||||
tenancy_ocid = os.getenv("OCI_CLI_TENANCY")
|
||||
key_content_b64 = os.getenv("OCI_CLI_KEY_CONTENT")
|
||||
region = os.getenv("OCI_CLI_REGION", "us-ashburn-1")
|
||||
|
||||
# Decode base64 private key
|
||||
try:
|
||||
key_content = base64.b64decode(key_content_b64).decode("utf-8")
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to decode OCI_CLI_KEY_CONTENT: {e}")
|
||||
|
||||
# Create OCI config dictionary
|
||||
config = {
|
||||
"user": user_ocid,
|
||||
"fingerprint": fingerprint,
|
||||
"tenancy": tenancy_ocid,
|
||||
"key_content": key_content,
|
||||
"region": region,
|
||||
}
|
||||
|
||||
# Initialize Identity client
|
||||
try:
|
||||
identity_client = oci.identity.IdentityClient(config)
|
||||
logging.info(f"Successfully authenticated to OCI region: {region}")
|
||||
return identity_client
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to create OCI Identity client: {e}")
|
||||
|
||||
|
||||
def fetch_oci_regions(identity_client):
|
||||
"""
|
||||
Fetch all OCI commercial regions using the Identity service API.
|
||||
|
||||
Args:
|
||||
identity_client (oci.identity.IdentityClient): Authenticated OCI client
|
||||
|
||||
Returns:
|
||||
dict: Dictionary mapping region identifiers to themselves (for consistency)
|
||||
|
||||
Raises:
|
||||
Exception: If API call fails
|
||||
"""
|
||||
logging.info("Fetching OCI commercial regions from Identity service")
|
||||
|
||||
try:
|
||||
# Call list_regions() API - returns all OC1 commercial regions
|
||||
response = identity_client.list_regions()
|
||||
regions = response.data
|
||||
|
||||
logging.info(f"Successfully fetched {len(regions)} regions from OCI API")
|
||||
|
||||
# Build region dictionary (region_id -> region_id)
|
||||
# Following AWS pattern: no display names, fully automated
|
||||
region_dict = {}
|
||||
for region in regions:
|
||||
region_id = region.name
|
||||
region_dict[region_id] = region_id
|
||||
logging.debug(f"Added region: {region_id}")
|
||||
|
||||
# Sort regions alphabetically by key
|
||||
region_dict = dict(sorted(region_dict.items()))
|
||||
|
||||
logging.info(f"Processed {len(region_dict)} commercial regions")
|
||||
return region_dict
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to fetch OCI regions: {e}")
|
||||
|
||||
|
||||
def update_config_file(regions, config_file_path):
|
||||
"""
|
||||
Update the OCI config file with new commercial regions while preserving government regions.
|
||||
|
||||
Args:
|
||||
regions (dict): Dictionary of region identifiers to region identifiers
|
||||
config_file_path (str): Path to the config.py file
|
||||
|
||||
Raises:
|
||||
Exception: If file operations fail or validation fails
|
||||
"""
|
||||
logging.info(f"Updating config file: {config_file_path}")
|
||||
|
||||
# Read current config file
|
||||
try:
|
||||
with open(config_file_path, "r") as f:
|
||||
config_content = f.read()
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to read config file: {e}")
|
||||
|
||||
# Generate new OCI_COMMERCIAL_REGIONS dictionary
|
||||
new_regions_dict = "OCI_COMMERCIAL_REGIONS = {\n"
|
||||
for region_id in regions.keys():
|
||||
new_regions_dict += f' "{region_id}": "{region_id}",\n'
|
||||
new_regions_dict += "}"
|
||||
|
||||
# Replace OCI_COMMERCIAL_REGIONS using regex
|
||||
pattern = r"OCI_COMMERCIAL_REGIONS\s*=\s*\{[^}]*\}"
|
||||
updated_content = re.sub(pattern, new_regions_dict, config_content)
|
||||
|
||||
# Validate that government regions still exist
|
||||
if "OCI_GOVERNMENT_REGIONS" not in updated_content:
|
||||
raise Exception(
|
||||
"Validation failed: OCI_GOVERNMENT_REGIONS section missing after update. Aborting to prevent data loss."
|
||||
)
|
||||
|
||||
# Verify the replacement was successful
|
||||
if updated_content == config_content:
|
||||
logging.warning("No changes detected in config file")
|
||||
return
|
||||
|
||||
# Write updated content back to file
|
||||
try:
|
||||
with open(config_file_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
logging.info("Successfully updated config file")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to write updated config file: {e}")
|
||||
|
||||
# Log summary of changes
|
||||
logging.info(f"Updated OCI_COMMERCIAL_REGIONS with {len(regions)} regions")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main execution function for OCI regions updater.
|
||||
"""
|
||||
try:
|
||||
# Setup OCI client with authentication
|
||||
identity_client = setup_oci_client()
|
||||
|
||||
# Fetch all commercial regions from OCI API
|
||||
commercial_regions = fetch_oci_regions(identity_client)
|
||||
|
||||
# Update config.py file
|
||||
config_file_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"..",
|
||||
"prowler",
|
||||
"providers",
|
||||
"oraclecloud",
|
||||
"config.py",
|
||||
)
|
||||
|
||||
update_config_file(commercial_regions, config_file_path)
|
||||
|
||||
logging.info("OCI regions update completed successfully")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error during OCI regions update: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user