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:
Andoni A. committed 2026-02-13 13:09:51 +01:00
commit 6b0a139361
68 files changed
+28314 -1246

No files matched your search

+1 -3
View File
@@ -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
+12
View File
@@ -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)
---
+93 -93
View File
@@ -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,
),
]
+10
View File
@@ -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)
+266
View File
@@ -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
+6
View File
@@ -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):
+38 -4
View File
@@ -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(
+23 -2
View File
@@ -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"],
},
]
}
)
+10
View File
@@ -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()
+4
View File
@@ -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
+7
View File
@@ -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,
)
+6
View File
@@ -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),
+31
View File
@@ -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"
)
+31
View File
@@ -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"
)
+31
View File
@@ -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.
+7 -2
View File
@@ -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
+39
View File
@@ -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
+13
View File
@@ -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,
]
+2 -2
View File
@@ -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.
+101
View File
@@ -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()
+3 -3
View File
@@ -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(
+13 -8
View File
@@ -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,
+3 -3
View File
@@ -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(
+5
View File
@@ -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)
---
+9 -32
View File
@@ -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 });
+5 -20
View File
@@ -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 {
+2 -5
View File
@@ -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(), {
+2 -6
View File
@@ -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(), {
+2 -6
View File
@@ -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(), {
+2 -5
View File
@@ -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(), {
+10 -12
View File
@@ -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
+15 -14
View File
@@ -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(), {
+9 -7
View File
@@ -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 && (
+9 -16
View File
@@ -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) => {
+1
View File
@@ -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
View File
@@ -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",
+14
View File
@@ -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 }),
},
});
/**
+143
View File
@@ -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: [],
})),
),
})),
);
};
+144
View File
@@ -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
View File
@@ -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",
+1012 -806
View File
File diff suppressed because it is too large. Load diff
+13
View File
@@ -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
+205
View File
@@ -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())