Compare commits

...
25 Commits
Author SHA1 Message Date
c076df74f9 fix(ui): handle pending compliance overview responses (#12366)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-08-06 12:26:51 +02:00
Prowler BotandJosema Camacho 5e812c614b fix(ui): correct GCP organization credential labels (#12364)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-08-06 12:13:13 +02:00
Prowler BotandJosema Camacho cd3d5630b7 fix(ui): launch organization scans through the bulk endpoint (#12359)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-08-06 11:03:28 +02:00
Prowler BotandHugo Pereira Brito e3adb38bb6 fix(alibabacloud): retry STS connection failures (#12360)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-08-06 09:53:57 +01:00
Prowler BotandCésar Arroba 1d3cefa490 fix(deps): upgrade cryptography to 50.0.0 (#12357)
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
2026-08-05 19:20:28 +02:00
Prowler BotandPedro Martín 5f94be2611 fix(aws): delegated administrator lookup and reporting (#12342)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-08-05 10:46:20 +02:00
065ce48c0f fix(sdk): only report JDBC strings with embedded credentials (#12338)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
2026-08-05 09:55:58 +02:00
Prowler BotandPedro Martín 8ee4ae2f46 fix(oci): handle null fields in identity service (#12337)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-08-04 17:20:13 +02:00
Prowler BotandAlejandro Bailo 2d535b19cc refactor(ui): type overview action results as ApiResult (#12331)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2026-08-04 15:08:43 +02:00
Prowler BotandAlejandro Bailo d480679638 fix(ui): harden Lighthouse context compiler against invalid items and budget pressure (#12330)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2026-08-04 14:43:05 +02:00
Prowler BotandAlejandro Bailo 6e24a49128 feat(ui): capture Lighthouse context on tenant admin pages (#12329)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2026-08-04 14:42:54 +02:00
Prowler BotandAlejandro Bailo a9a60c9448 fix(ui): guard Overview Lighthouse context against API error responses (#12328)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2026-08-04 14:42:23 +02:00
Prowler Botandprowler-bot a180d8a775 chore(release): Bump versions to v5.37.2 (#12325)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-04 13:23:49 +02:00
César Arroba 43ecebc849 fix(api): restore the API version on the release branch (#12324) 2026-08-04 12:19:07 +02:00
Prowler BotandAlan Buscaglia a70a69a7d0 fix(ui): stabilize overview responsive layout (#12318)
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
2026-08-04 12:12:59 +02:00
6d7b1ef153 chore(changelog): v5.37.1 (#12321)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
Co-authored-by: César Arroba <cesar@prowler.com>
2026-08-04 12:11:35 +02:00
Prowler BotandDaniel Barranquero f26c814e5a fix(m365): bump microsoft-kiota packages to 1.9.10 so guest-user CA checks parse guestOrExternalUserTypes (#12315)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
2026-08-04 11:49:55 +02:00
Prowler BotBruno FerreiraCursorcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Utwo
587f9bcfa6 fix(app): correct the worker KEDA PostgreSQL scaler (#12316)
Co-authored-by: Bruno Ferreira <bmibferreira@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Utwo <mihai.legat@gmail.com>
2026-08-04 10:51:08 +02:00
Prowler BotStylusFrostcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Daniel Barranquero
b12aa854d6 fix(sdk): resolve entry-point checks on built-in providers (#12312)
Co-authored-by: StylusFrost <43682773+StylusFrost@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-08-04 10:33:02 +02:00
César Arroba 407ce0d4ca fix(container): backport the api image CVE fixes to v5.37 (#12311) 2026-08-04 10:13:03 +02:00
fbbf9fe1cd fix(container): clear the Private Cloud image vulnerabilities (PROWLER-2291) (#12307)
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
Co-authored-by: César Arroba <cesar@prowler.com>
2026-08-04 10:05:59 +02:00
Prowler BotandMaringanti Vasist Acharya ca9feed805 fix(sdk): renumber Huawei Cloud off E2E Networks' range, and correct the M365 header comment (#12306)
Co-authored-by: Maringanti Vasist Acharya <150829100+VasistAcharya@users.noreply.github.com>
2026-08-04 09:47:15 +02:00
Prowler Botandprowler-bot 234e69da3f chore(release): Bump versions to v5.37.1 (#12303)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-03 18:19:48 +02:00
Prowler Botandprowler-bot f1b3b76efc chore(api): Update prowler dependency to v5.37 for release 5.37.0 (#12298)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-08-03 15:16:38 +02:00
5cd673787b fix(ci): use bot token for release freeze (#12297)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-08-03 14:50:54 +02:00
130 changed files with 4978 additions and 1467 deletions
+1 -1
View File
@@ -158,7 +158,7 @@ SENTRY_RELEASE=local
# REO_DEV_CLIENT_ID=
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.37.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.37.2
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
+2 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Enable release freeze
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
run: |
gh variable set RELEASE_FREEZE --body true --repo "${GITHUB_REPOSITORY}"
@@ -389,3 +389,4 @@ jobs:
if: always()
run: |
rm -f prowler_changelog.md api_changelog.md ui_changelog.md mcp_changelog.md combined_changelog.md
+62
View File
@@ -0,0 +1,62 @@
# Findings excluded from the Grype gate, each with a reason.
# Anything not listed here blocks the pull request at critical or high severity.
# Pairs are explicit: a new CVE against an already-listed package still blocks.
#
# Every entry below has a published fix we cannot take. Findings with no fix at all are
# not listed: the scan runs with only-fixed, so they never reach the gate.
ignore:
# Modules compiled into the Trivy binary we ship.
# Only a Trivy rebuild by its vendor can change these; the version is pinned in our Dockerfile.
- vulnerability: CVE-2026-56852
package:
name: golang.org/x/text
- vulnerability: GHSA-hrxh-6v49-42gf
package:
name: google.golang.org/grpc
- vulnerability: CVE-2026-50151
package:
name: oras.land/oras-go/v2
# Shipped inside the PowerShell tarball, in its bundled MicrosoftTeams module.
# Not a dependency we declare, and not one we can upgrade independently.
- vulnerability: CVE-2026-26127
package:
name: Microsoft.Bcl.Memory
# The CPython interpreter, compiled into the official base image.
# TEMPORARY, unlike the entries above: moving to Python 3.13 clears seven of these, and
# that is a runtime upgrade pending its own evaluation. The remaining three need 3.15 and
# are unfixable either way -- the MCP image already runs 3.13.14 and still reports them.
- vulnerability: CVE-2026-11940
package:
name: python
- vulnerability: CVE-2026-11972
package:
name: python
- vulnerability: CVE-2026-15308
package:
name: python
- vulnerability: CVE-2026-3298
package:
name: python
- vulnerability: CVE-2026-3644
package:
name: python
- vulnerability: CVE-2026-4224
package:
name: python
- vulnerability: CVE-2026-4786
package:
name: python
- vulnerability: CVE-2026-6100
package:
name: python
- vulnerability: CVE-2026-7210
package:
name: python
- vulnerability: CVE-2026-9669
package:
name: python
+50 -115
View File
@@ -1,123 +1,58 @@
# Trivy ignore file for prowlercloud/prowler SDK container image.
# Trivy ignore file for the prowlercloud/prowler SDK and API container images.
# Each entry below documents (a) the affected package and why it ships in the
# image, (b) why the CVE is not exploitable in Prowler's runtime, and (c) the
# upstream fix status. Entries carry an expiry so they auto-force re-review.
# Entries are scoped per-package so suppressions cannot drift onto unrelated
# packages that may be assigned the same CVE in the future.
# The `pkg:` selector on each line is documentation only. Trivy's classic
# .trivyignore format parses the CVE ID and ignores the rest, so each entry
# suppresses its CVE across every package in the image, not just the one named.
# Verified against Trivy 0.65.0: an entry written `pkg:zlib1g` still suppressed
# the finding on perl-base. Real per-package scoping needs .trivyignore.yaml
# with purls — tracked in PROWLER-2327.
# `exp:` IS honoured: an entry dated in the past correctly lapses.
#
# Keep expiries staggered, and only suppress packages the images actually install.
#
# Scanned by: .github/actions/trivy-scan via .github/workflows/sdk-container-checks.yml
# and .github/workflows/api-container-checks.yml
# CVE-2026-42496 — perl-archive-tar path traversal via crafted symlinks.
# CVE-2026-8376 — perl heap buffer overflow when compiling regex.
# Packages: perl, perl-base, perl-modules-5.36, libperl5.36.
# Why ignored: perl-base is part of Debian's "Essential: yes" set; it cannot be
# removed without breaking dpkg. The Prowler SDK does not invoke perl at runtime;
# neither vulnerable code path (Archive::Tar parsing or regex compilation of
# attacker-controlled input) is reachable from Prowler. No Debian bookworm fix
# is available yet.
CVE-2026-42496 pkg:perl exp:2026-08-15
CVE-2026-42496 pkg:perl-base exp:2026-08-15
CVE-2026-42496 pkg:perl-modules-5.36 exp:2026-08-15
CVE-2026-42496 pkg:libperl5.36 exp:2026-08-15
CVE-2026-8376 pkg:perl exp:2026-08-15
CVE-2026-8376 pkg:perl-base exp:2026-08-15
CVE-2026-8376 pkg:perl-modules-5.36 exp:2026-08-15
CVE-2026-8376 pkg:libperl5.36 exp:2026-08-15
# perl-base is Debian "Essential: yes". Trivy spreads src:perl CVEs across every
# binary package, so perl-base gets flagged for modules only perl-modules-5.40 ships.
# Neither image installs perl-modules-5.40, and nothing in either invokes perl.
#
# Why these four cannot be fixed rather than accepted (reviewed 2026-07-31):
#
# 1. No fix exists. All four report no fixed version on perl-base 5.40.1-6.
# Debian's tracker marks CVE-2026-42496 "fix_deferred" and the other three
# "affected". Updating the base image, apt upgrade, or moving to a newer
# Debian release changes nothing, because Debian has not shipped a fix.
# 2. The package cannot be removed. "Essential: yes" means removal needs
# dpkg --force-remove-essential, which also breaks apt for anything built
# downstream from these images.
# 3. Changing base distribution was evaluated and rejected. Alpine removes
# perl entirely, but PowerShell publishes no linux-musl-arm64 build in any
# release, so M365 scanning would break on arm64 — which is what we run in
# production. Wolfi keeps glibc and drops perl, but pinnable versioned tags
# are a paid tier, so builds would not be reproducibly pinnable.
#
# Not-invoked claim verified by sweeping both images for: files with a perl
# shebang, shell/python callers of perl, ELF binaries containing "perl", and
# .pl/.pm files or perl subprocess calls anywhere in site-packages. The only
# consumers found are dpkg/debconf/adduser/pam tooling, none of which runs at
# runtime, plus one build-time script inside the ExchangeOnlineManagement
# PowerShell module that is never invoked (it generates that module's manifest,
# and lives in its netFramework/ path, while Linux loads netCore/).
# CVE-2026-13221 - Perl regex trie overflow.
# Packages: perl, perl-base, perl-modules-5.36, libperl5.36.
# Why ignored: upstream confirms Perl 5.36.0 is not affected; the regression
# was introduced after this version. Debian currently marks bookworm as
# vulnerable, which causes Trivy to report a false positive.
# Archive::Tar path traversal. Not installed: `perl -MArchive::Tar -e1` cannot locate it.
CVE-2026-42496 pkg:perl-base exp:2027-01-31
# Storable integer overflow. Not installed: `perl -MStorable -e1` cannot locate it.
CVE-2026-57433 pkg:perl-base exp:2027-01-31
# Regex heap overflow on 32-bit builds only; both published arches are 64-bit.
CVE-2026-8376 pkg:perl-base exp:2027-01-31
# Regex trie bug giving silently wrong matches above 65535 alternation branches. Now on
# perl 5.40.1, which is in range (the 5.36-predates-it argument no longer applies), so this
# rests on nothing invoking perl. Short expiry to force a re-look.
# Ref: https://github.com/Perl/perl5/issues/23388
CVE-2026-13221 pkg:perl exp:2026-08-15
CVE-2026-13221 pkg:perl-base exp:2026-08-15
CVE-2026-13221 pkg:perl-modules-5.36 exp:2026-08-15
CVE-2026-13221 pkg:libperl5.36 exp:2026-08-15
# CVE-2026-57433 — Perl Storable signed integer overflow when deserializing a
# crafted SX_HOOK record (retrieve_hook_common passes a wrapped negative count
# to av_extend).
# Packages: perl, perl-base, perl-modules-5.36, libperl5.36.
# Why ignored: perl-base is part of Debian's "Essential: yes" set; it cannot be
# removed without breaking dpkg. Prowler does not invoke perl at runtime and
# never calls Storable's thaw/retrieve on attacker-controlled blobs, so the
# vulnerable deserialization path is unreachable. Fixed upstream in
# Storable 3.41; no Debian bookworm fix is available yet.
CVE-2026-57433 pkg:perl exp:2026-08-15
CVE-2026-57433 pkg:perl-base exp:2026-08-15
CVE-2026-57433 pkg:perl-modules-5.36 exp:2026-08-15
CVE-2026-57433 pkg:libperl5.36 exp:2026-08-15
# CVE-2025-7458 — SQLite integer overflow.
# Package: libsqlite3-0.
# Why ignored: transitive dependency of CPython's stdlib sqlite3 module. The
# Prowler SDK does not open user-supplied SQLite databases; SQLite usage is
# internal and bounded. No Debian bookworm fix is available.
CVE-2025-7458 pkg:libsqlite3-0 exp:2026-08-15
# CVE-2026-43185 — Linux kernel ksmbd signedness bug.
# Package: linux-libc-dev.
# Why ignored: linux-libc-dev ships kernel headers for build-time compilation,
# not a running kernel. Containers execute against the host kernel, so these
# headers are inert at runtime. The upstream fix landed in kernel 7.0-rc2 and
# has not been backported to Debian's 6.1 LTS line.
CVE-2026-43185 pkg:linux-libc-dev exp:2026-08-15
# CVE-2023-45853 — zlib MiniZip integer overflow / heap overflow in
# zipOpenNewFileInZip4_64.
# Packages: zlib1g, zlib1g-dev.
# Why ignored: Debian Security Tracker status for bookworm is <ignored>, with
# the published rationale "contrib/minizip not built and src:zlib not producing
# binary packages" — i.e. the vulnerable symbol is not present in the libz.so
# shipped by Debian. Real-not-affected, not unpatched. Upstream fix is in
# zlib 1.3.1, available in Debian trixie (13); migrating the base image would
# clear it fully.
# Ref: https://security-tracker.debian.org/tracker/CVE-2023-45853
CVE-2023-45853 pkg:zlib1g exp:2026-08-15
CVE-2023-45853 pkg:zlib1g-dev exp:2026-08-15
# CVE-2026-55200 — libssh2 out-of-bounds write in ssh2_transport_read() due to
# an unchecked packet_length field in transport.c (heap corruption, possible RCE).
# Package: libssh2-1.
# Why ignored: libssh2-1 is pulled in only as a transitive dependency of libcurl4
# (installed in the SDK Dockerfile for the networking/PowerShell stack). The
# vulnerable path is reached exclusively when libssh2 acts as an SSH/SCP/SFTP
# client parsing transport packets from a server. Prowler never uses libcurl's
# SSH/SCP/SFTP transports; it talks to cloud provider HTTPS endpoints only, so the
# affected code is unreachable at runtime. Fixed upstream in libssh2 commit
# 97acf3df (PR #2052); no Debian bookworm fix is available yet.
# Ref: https://security-tracker.debian.org/tracker/CVE-2026-55200
CVE-2026-55200 pkg:libssh2-1 exp:2026-08-15
# --- API container image (api/Dockerfile) ---
# The entries below are specific to the Prowler API image, which ships
# PowerShell and additional build tooling on top of the same bookworm base.
# CVE-2026-7210 — CPython/Expat hash-flooding denial of service in
# `xml.parsers.expat` and `xml.etree.ElementTree`.
# Packages: the Debian system Python 3.11 (python3.11*, libpython3.11*).
# Why ignored: the API runs under the Python 3.12 interpreter shipped in its
# `.venv`; the system `python3.11` is only present because `python3-dev` is
# pulled in to compile native extensions (xmlsec, lxml) and is never executed
# at runtime. The vulnerable path requires parsing attacker-controlled XML with
# the affected interpreter, which Prowler does not do with the system Python.
# Full mitigation also needs libexpat >= 2.8.0; no Debian bookworm fix yet.
CVE-2026-7210 pkg:python3.11 exp:2026-08-15
CVE-2026-7210 pkg:python3.11-dev exp:2026-08-15
CVE-2026-7210 pkg:python3.11-minimal exp:2026-08-15
CVE-2026-7210 pkg:libpython3.11 exp:2026-08-15
CVE-2026-7210 pkg:libpython3.11-dev exp:2026-08-15
CVE-2026-7210 pkg:libpython3.11-minimal exp:2026-08-15
CVE-2026-7210 pkg:libpython3.11-stdlib exp:2026-08-15
# CVE-2026-33278 — Unbound DNSSEC validator use-after-free (DoS, possible RCE).
# CVE-2026-42960 — Unbound DNS cache poisoning via promiscuous additional records.
# Package: libunbound8.
# Why ignored: libunbound8 is a transitive apt dependency of the TLS/networking
# stack (GnuTLS DANE support); only the shared library ships in the image. Both
# vulnerabilities require operating a live Unbound recursive DNSSEC validator
# that processes attacker-influenced DNS responses. Prowler never starts an
# Unbound resolver, so neither code path is reachable. No Debian bookworm fix yet.
CVE-2026-33278 pkg:libunbound8 exp:2026-08-15
CVE-2026-42960 pkg:libunbound8 exp:2026-08-15
CVE-2026-13221 pkg:perl-base exp:2026-11-30
+143
View File
@@ -0,0 +1,143 @@
# Trivy suppressions for the prowlercloud/prowler SDK and API container images.
#
# This file replaces the classic .trivyignore, which parsed only the CVE id: the
# `pkg:` selector written on each line was documentation and the entry suppressed
# its CVE across every package in the image. The `purls` field below is honoured,
# so each entry is scoped to the package it names. Verified against Trivy 0.71.2:
# an entry given the wrong purl leaves the finding reported, where the classic
# format suppressed it.
#
# `expired_at` forces re-review. Keep the dates staggered.
#
# The four entries below are currently redundant: the scan runs with ignore-unfixed,
# and none of them has a published fix, so they never reach the gate either way. They
# are kept because the reasoning is what justifies accepting them, and because they
# apply again the moment any of them gains a fix we do not take.
#
# perl-base is Debian "Essential: yes". Trivy spreads src:perl CVEs across every
# binary package built from that source, so perl-base is flagged for modules only
# perl-modules-* ships. Neither image installs those, and nothing in either
# invokes perl.
#
# Why these four are accepted rather than fixed (reviewed 2026-07-31):
#
# 1. No fix exists. All four report no fixed version on perl-base 5.40.1-6.
# Debian marks CVE-2026-42496 "fix_deferred" and the other three "affected".
# A newer base image, apt upgrade, or a newer Debian release changes nothing.
# 2. The package cannot be removed. "Essential: yes" means removal needs
# dpkg --force-remove-essential, which breaks apt for anything built
# downstream from these images.
# 3. Changing base distribution was evaluated and rejected. Alpine drops perl
# entirely, but PowerShell publishes no linux-musl-arm64 build in any
# release, so M365 scanning would break on arm64 -- which is what we run in
# production. Wolfi keeps glibc and drops perl, but pinnable versioned tags
# are a paid tier, so builds would not be reproducibly pinnable.
#
# Not-invoked claim verified by sweeping both images for files with a perl
# shebang, shell/python callers of perl, ELF binaries containing "perl", and
# .pl/.pm files or perl subprocess calls anywhere in site-packages. The only
# consumers found are dpkg/debconf/adduser/pam tooling, none of which runs at
# runtime, plus one build-time script inside the ExchangeOnlineManagement
# PowerShell module that is never invoked.
vulnerabilities:
# Archive::Tar path traversal. Not installed: `perl -MArchive::Tar -e1` cannot locate it.
- id: CVE-2026-42496
purls:
- "pkg:deb/debian/perl-base"
expired_at: 2027-01-31
# Storable integer overflow. Not installed: `perl -MStorable -e1` cannot locate it.
- id: CVE-2026-57433
purls:
- "pkg:deb/debian/perl-base"
expired_at: 2027-01-31
# Regex heap overflow on 32-bit builds only; both published arches are 64-bit.
- id: CVE-2026-8376
purls:
- "pkg:deb/debian/perl-base"
expired_at: 2027-01-31
# Regex trie bug giving silently wrong matches above 65535 alternation branches.
# perl 5.40.1 is in range, so this rests on nothing invoking perl. Short expiry
# to force a re-look. Ref: https://github.com/Perl/perl5/issues/23388
- id: CVE-2026-13221
purls:
- "pkg:deb/debian/perl-base"
expired_at: 2026-11-30
# Declared in the SPDX manifest that ships inside PowerShell's MicrosoftTeams module
# (Modules/MicrosoftTeams/7.9.0/_manifest/spdx_2.2/manifest.spdx.json). Trivy reads that
# SBOM and reports what it declares, which is not the same as what the image contains:
# there is no Node runtime and no node_modules anywhere in the image, and the .NET
# assemblies target net472, a Windows-only framework. Nothing here is reachable, and none
# of it is a dependency we declare -- only Microsoft can change the module's contents.
- id: CVE-2020-0606
purls:
- "pkg:nuget/Microsoft.WindowsDesktop.App.Ref"
expired_at: 2027-01-31
- id: CVE-2019-0820
purls:
- "pkg:nuget/System.Text.RegularExpressions"
expired_at: 2027-01-31
- id: CVE-2026-47302
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
- id: CVE-2026-47304
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
- id: CVE-2026-50525
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
- id: CVE-2026-50527
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
- id: CVE-2026-50648
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
- id: CVE-2026-13676
purls:
- "pkg:npm/fast-uri"
expired_at: 2027-01-31
- id: CVE-2026-16221
purls:
- "pkg:npm/fast-uri"
expired_at: 2027-01-31
- id: CVE-2026-18446
purls:
- "pkg:npm/fast-uri"
expired_at: 2027-01-31
- id: CVE-2026-69192
purls:
- "pkg:npm/ip-address"
expired_at: 2027-01-31
# Modules compiled into the Trivy binary the images ship. The binary is pinned by version
# and verified by checksum in the Dockerfile; only a rebuild by its vendor moves these.
- id: CVE-2026-56852
purls:
- "pkg:golang/golang.org/x/text"
expired_at: 2026-12-31
- id: GHSA-hrxh-6v49-42gf
purls:
- "pkg:golang/google.golang.org/grpc"
expired_at: 2026-12-31
- id: CVE-2026-50151
purls:
- "pkg:golang/oras.land/oras-go/v2"
expired_at: 2026-12-31
- id: CVE-2026-50163
purls:
- "pkg:golang/oras.land/oras-go/v2"
expired_at: 2026-12-31
- id: CVE-2026-39822
purls:
- "pkg:golang/stdlib"
expired_at: 2026-12-31
+18 -5
View File
@@ -1,14 +1,14 @@
FROM python:3.12.13-slim-bookworm@sha256:8a7e7cc04fd3e2bd787f7f24e22d5d119aa590d429b50c95dfe12b3abe52f48b AS build
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build
LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
ARG POWERSHELL_VERSION=7.5.0
ARG POWERSHELL_VERSION=7.5.9
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.71.2
ARG TRIVY_VERSION=0.72.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
@@ -16,7 +16,7 @@ ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
wget libicu72 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \
wget libicu76 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \
build-essential pkg-config libzstd-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
@@ -89,7 +89,7 @@ ENV HOME='/home/prowler'
ENV PATH="${HOME}/.local/bin:${PATH}"
#hadolint ignore=DL3013
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir uv==0.11.14
pip install --no-cache-dir uv==0.12.0
RUN uv sync --locked --compile-bytecode && \
rm -rf ~/.cache/uv
@@ -105,6 +105,9 @@ RUN apt-get purge -y --auto-remove \
pkg-config \
libzstd-dev \
zlib1g-dev \
wget \
gnupg \
apt-transport-https \
&& rm -rf /var/lib/apt/lists/*
USER prowler
@@ -113,5 +116,15 @@ USER prowler
RUN pip uninstall dash-html-components -y && \
pip uninstall dash-core-components -y
USER root
# pip is build-only; the entrypoint runs the venv directly.
RUN rm -rf /usr/local/lib/python3.12/site-packages/pip \
/usr/local/lib/python3.12/site-packages/pip-*.dist-info \
/home/prowler/.local/lib/python3.12/site-packages/pip \
/home/prowler/.local/lib/python3.12/site-packages/pip-*.dist-info \
/usr/local/bin/pip /usr/local/bin/pip3 /usr/local/bin/pip3.12 \
/home/prowler/.local/bin/pip /home/prowler/.local/bin/pip3 /home/prowler/.local/bin/pip3.12
USER prowler
ENTRYPOINT ["/home/prowler/.venv/bin/prowler"]
+18
View File
@@ -4,6 +4,24 @@ All notable changes to the **Prowler API** are documented in this file.
<!-- changelog: release notes start -->
## [1.38.1] (Prowler v5.37.1)
### 🐞 Fixed
- Entra Conditional Access guest-user checks no longer report false FAILs in M365 scans: microsoft-kiota packages overridden to 1.9.10 so `guestOrExternalUserTypes` (a flags enum Graph serializes as a comma-separated string) deserializes correctly instead of returning an empty list [(#12315)](https://github.com/prowler-cloud/prowler/pull/12315)
### 🔐 Security
- The API container image now builds on Debian 13 (trixie), taking its critical CVE count from 18 to 4 [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
- Bumped PowerShell, Trivy and uv in the API container image, clearing 14 high-severity CVEs [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
- Bumped `workos` and `pyopenssl` so the API can move to `cryptography` 48.0.1 [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
- Removed `gnupg` and `apt-transport-https` from the API container image [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
- The API container image no longer ships `git`; removing it also dropped `perl`, `perl-modules`, `libperl` and `liberror-perl`, clearing 12 critical CVEs. Only `perl-base` remains, which Debian marks Essential and cannot be removed [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
- Removed `pip` from the API container image, clearing two high-severity CVEs in the vendored copies of `setuptools` and `msgpack` [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
- Bumped `pillow` to 12.3.0, `httplib2` to 0.32.0 and `pyasn1` to 0.6.4 to resolve known CVEs [(#12311)](https://github.com/prowler-cloud/prowler/pull/12311)
---
## [1.38.0] (Prowler v5.37.0)
### 🚀 Added
+20 -6
View File
@@ -1,13 +1,13 @@
FROM python:3.12.13-slim-bookworm@sha256:8a7e7cc04fd3e2bd787f7f24e22d5d119aa590d429b50c95dfe12b3abe52f48b AS build
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build
LABEL maintainer="https://github.com/prowler-cloud/api"
ARG POWERSHELL_VERSION=7.5.0
ARG POWERSHELL_VERSION=7.5.9
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.71.2
ARG TRIVY_VERSION=0.72.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
@@ -17,7 +17,7 @@ ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
git \
libicu72 \
libicu76 \
gcc \
g++ \
make \
@@ -28,7 +28,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libtool \
libxslt1-dev \
python3-dev \
git \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
@@ -94,7 +93,7 @@ RUN mkdir -p /tmp/prowler_api_output
COPY --chown=prowler:prowler pyproject.toml uv.lock ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir uv==0.11.14
pip install --no-cache-dir uv==0.12.0
ENV PATH="/home/prowler/.local/bin:$PATH"
@@ -109,19 +108,34 @@ RUN .venv/bin/python -m prowler.providers.m365.lib.powershell.m365_powershell
USER root
# Remove build-only packages from the final image after Python dependencies are installed.
# git is only needed by uv sync for the `prowler @ git+...` dependency; purging it drops perl too.
# wget stays: the compose healthcheck shells out to it.
RUN apt-get purge -y --auto-remove \
gcc \
g++ \
git \
make \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libxmlsec1t64 \
libxmlsec1t64-openssl \
pkg-config \
libtool \
libxslt1-dev \
python3-dev \
gnupg \
apt-transport-https \
&& rm -rf /var/lib/apt/lists/*
# pip is build-only; the entrypoint runs uv against the prepared venv. uv stays.
RUN rm -rf /usr/local/lib/python3.12/site-packages/pip \
/usr/local/lib/python3.12/site-packages/pip-*.dist-info \
/home/prowler/.local/lib/python3.12/site-packages/pip \
/home/prowler/.local/lib/python3.12/site-packages/pip-*.dist-info \
/usr/local/bin/pip /usr/local/bin/pip3 /usr/local/bin/pip3.12 \
/home/prowler/.local/bin/pip /home/prowler/.local/bin/pip3 /home/prowler/.local/bin/pip3.12
USER prowler
COPY --chown=prowler:prowler src/backend/ ./backend/
@@ -0,0 +1 @@
Upgrade cryptography to 50.0.0, closing CVE-2026-69247 and CVE-2026-69249
+38 -25
View File
@@ -45,7 +45,7 @@ dependencies = [
"gunicorn==26.0.0",
"uvloop==0.22.1",
"lxml==6.1.0",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.37",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
@@ -63,7 +63,7 @@ dependencies = [
"werkzeug (==3.1.7)",
"sqlparse (==0.5.5)",
"fonttools (==4.62.1)",
"uvicorn-worker (==0.4.0)",
"uvicorn-worker (==0.4.0)"
]
description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
@@ -71,7 +71,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.38.0"
version = "1.38.2"
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
# target-version tracks this project's lowest supported Python.
@@ -92,6 +92,8 @@ extend-select = [
[tool.uv]
# Transitive pins matching master to avoid silent drift; bump deliberately.
# workos and pyopenssl run ahead of master: the versions master pins cap cryptography
# below 48, so both were bumped to versions that allow it (PROWLER-2310).
constraint-dependencies = [
"about-time==4.2.1",
"adal==1.2.7",
@@ -128,7 +130,7 @@ constraint-dependencies = [
"alibabacloud-sls20201230==5.9.0",
"alibabacloud-sts20150401==1.1.6",
"alibabacloud-tea==0.4.3",
"alibabacloud-tea-openapi==0.4.4",
"alibabacloud-tea-openapi==0.4.5",
"alibabacloud-tea-util==0.3.14",
"alibabacloud-tea-xml==0.0.3",
"alibabacloud-vpc20160428==6.13.0",
@@ -210,9 +212,9 @@ constraint-dependencies = [
"coverage==7.5.4",
"cron-descriptor==1.4.5",
"crowdstrike-falconpy==1.6.0",
"cryptography==46.0.7",
"cryptography==50.0.0",
"cycler==0.12.1",
"darabonba-core==1.0.5",
"darabonba-core==1.0.8",
"dash==3.1.1",
"dash-bootstrap-components==2.0.3",
"debugpy==1.8.20",
@@ -277,7 +279,7 @@ constraint-dependencies = [
"h2==4.3.0",
"hpack==4.1.0",
"httpcore==1.0.9",
"httplib2==0.31.2",
"httplib2==0.32.0",
"httpx==0.28.1",
"humanfriendly==10.0",
"hyperframe==6.1.0",
@@ -314,13 +316,13 @@ constraint-dependencies = [
"matplotlib==3.10.8",
"mccabe==0.7.0",
"mdurl==0.1.2",
"microsoft-kiota-abstractions==1.9.9",
"microsoft-kiota-authentication-azure==1.9.9",
"microsoft-kiota-http==1.9.9",
"microsoft-kiota-serialization-form==1.9.9",
"microsoft-kiota-serialization-json==1.9.9",
"microsoft-kiota-serialization-multipart==1.9.9",
"microsoft-kiota-serialization-text==1.9.9",
"microsoft-kiota-abstractions==1.9.10",
"microsoft-kiota-authentication-azure==1.9.10",
"microsoft-kiota-http==1.9.10",
"microsoft-kiota-serialization-form==1.9.10",
"microsoft-kiota-serialization-json==1.9.10",
"microsoft-kiota-serialization-multipart==1.9.10",
"microsoft-kiota-serialization-text==1.9.10",
"microsoft-security-utilities-secret-masker==1.0.0b4",
"msal==1.35.0b1",
"msal-extensions==1.2.0",
@@ -337,7 +339,7 @@ constraint-dependencies = [
"nltk==3.9.4",
"numpy==2.2.6",
"oauthlib==3.3.1",
"oci==2.169.0",
"oci==2.183.0",
"openai==1.109.1",
"openstacksdk==4.2.0",
"opentelemetry-api==1.39.1",
@@ -349,7 +351,7 @@ constraint-dependencies = [
"pagerduty==6.1.0",
"pandas==2.2.3",
"pbr==7.0.3",
"pillow==12.2.0",
"pillow==12.3.0",
"pkginfo==1.12.1.2",
"platformdirs==4.5.1",
"plotly==6.5.2",
@@ -365,8 +367,8 @@ constraint-dependencies = [
"psycopg2-binary==2.9.9",
"py-deviceid==0.1.1",
"py-iam-expand==0.3.0",
"py-ocsf-models==0.8.1",
"pyasn1==0.6.3",
"py-ocsf-models==0.10.0",
"pyasn1==0.6.4",
"pyasn1-modules==0.4.2",
"pycodestyle==2.14.0",
"pycparser==3.0",
@@ -378,7 +380,7 @@ constraint-dependencies = [
"pylint==3.2.5",
"pymsalruntime==0.18.1",
"pynacl==1.6.2",
"pyopenssl==26.0.0",
"pyopenssl==26.2.0",
"pyparsing==3.3.2",
"pyreadline3==3.5.4",
"pysocks==1.7.1",
@@ -447,7 +449,7 @@ constraint-dependencies = [
"wcwidth==0.5.3",
"websocket-client==1.9.0",
"werkzeug==3.1.7",
"workos==6.0.8",
"workos==8.3.0",
"wrapt==1.17.3",
"xlsxwriter==3.2.9",
"xmlsec==1.3.17",
@@ -466,10 +468,13 @@ constraint-dependencies = [
# 0.138.1 requires azure-mgmt-containerservice>=41.0.0. Attack Paths does not
# ingest Azure today, so override the Cartography dependency to the Prowler pin.
#
# prowler@master hard-pins microsoft-kiota-abstractions==1.9.2 in [project.dependencies].
# The microsoft-kiota-http security bump to 1.9.9 (GHSA-7j59-v9qr-6fq9) requires
# microsoft-kiota-abstractions>=1.9.9, which a constraint cannot satisfy against the
# SDK's hard pin; override it to the patched, kiota-aligned version.
# prowler@master hard-pins the microsoft-kiota packages in [project.dependencies].
# microsoft-kiota-serialization-json 1.9.10 fixes get_collection_of_enum_values
# returning [] for flags enums serialized as CSV strings (microsoft/kiota-python#515),
# which broke the Entra Conditional Access guest-user checks; the kiota packages
# release in lockstep and 1.9.10 requires microsoft-kiota-abstractions>=1.9.10, which
# a constraint cannot satisfy against the SDK's hard pins, so override the whole set
# to 1.9.10 until the SDK bump propagates to the pinned master rev.
#
# prowler@master hard-pins dulwich==0.23.0 and pyjwt==2.12.1 in [project.dependencies].
# dulwich 1.2.5 patches GHSA-897w-fcg9-f6xj (arbitrary file write) and pyjwt 2.13.0
@@ -480,8 +485,16 @@ constraint-dependencies = [
# that request pyjwt[crypto] and leave cryptography (needed for RS256) only transitive.
override-dependencies = [
"okta==3.4.2",
# alibabacloud-tea-openapi 0.4.5 caps cryptography below 49 and is the latest release.
"cryptography==50.0.0",
"azure-mgmt-containerservice==34.1.0",
"microsoft-kiota-abstractions==1.9.9",
"microsoft-kiota-abstractions==1.9.10",
"microsoft-kiota-authentication-azure==1.9.10",
"microsoft-kiota-http==1.9.10",
"microsoft-kiota-serialization-form==1.9.10",
"microsoft-kiota-serialization-json==1.9.10",
"microsoft-kiota-serialization-multipart==1.9.10",
"microsoft-kiota-serialization-text==1.9.10",
"dulwich==1.2.5",
"pyjwt[crypto]==2.13.0"
]
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.38.0
version: 1.38.2
description: |-
Prowler API specification.
Generated
+380 -134
View File
@@ -45,7 +45,7 @@ constraints = [
{ name = "alibabacloud-sls20201230", specifier = "==5.9.0" },
{ name = "alibabacloud-sts20150401", specifier = "==1.1.6" },
{ name = "alibabacloud-tea", specifier = "==0.4.3" },
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.4" },
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.5" },
{ name = "alibabacloud-tea-util", specifier = "==0.3.14" },
{ name = "alibabacloud-tea-xml", specifier = "==0.0.3" },
{ name = "alibabacloud-vpc20160428", specifier = "==6.13.0" },
@@ -127,9 +127,9 @@ constraints = [
{ name = "coverage", specifier = "==7.5.4" },
{ name = "cron-descriptor", specifier = "==1.4.5" },
{ name = "crowdstrike-falconpy", specifier = "==1.6.0" },
{ name = "cryptography", specifier = "==46.0.7" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "cycler", specifier = "==0.12.1" },
{ name = "darabonba-core", specifier = "==1.0.5" },
{ name = "darabonba-core", specifier = "==1.0.8" },
{ name = "dash", specifier = "==3.1.1" },
{ name = "dash-bootstrap-components", specifier = "==2.0.3" },
{ name = "debugpy", specifier = "==1.8.20" },
@@ -194,7 +194,7 @@ constraints = [
{ name = "h2", specifier = "==4.3.0" },
{ name = "hpack", specifier = "==4.1.0" },
{ name = "httpcore", specifier = "==1.0.9" },
{ name = "httplib2", specifier = "==0.31.2" },
{ name = "httplib2", specifier = "==0.32.0" },
{ name = "httpx", specifier = "==0.28.1" },
{ name = "humanfriendly", specifier = "==10.0" },
{ name = "hyperframe", specifier = "==6.1.0" },
@@ -231,13 +231,13 @@ constraints = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "mccabe", specifier = "==0.7.0" },
{ name = "mdurl", specifier = "==0.1.2" },
{ name = "microsoft-kiota-abstractions", specifier = "==1.9.9" },
{ name = "microsoft-kiota-authentication-azure", specifier = "==1.9.9" },
{ name = "microsoft-kiota-http", specifier = "==1.9.9" },
{ name = "microsoft-kiota-serialization-form", specifier = "==1.9.9" },
{ name = "microsoft-kiota-serialization-json", specifier = "==1.9.9" },
{ name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.9" },
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.9" },
{ name = "microsoft-kiota-abstractions", specifier = "==1.9.10" },
{ name = "microsoft-kiota-authentication-azure", specifier = "==1.9.10" },
{ name = "microsoft-kiota-http", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-form", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-json", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" },
{ name = "microsoft-security-utilities-secret-masker", specifier = "==1.0.0b4" },
{ name = "msal", specifier = "==1.35.0b1" },
{ name = "msal-extensions", specifier = "==1.2.0" },
@@ -254,7 +254,7 @@ constraints = [
{ name = "nltk", specifier = "==3.9.4" },
{ name = "numpy", specifier = "==2.2.6" },
{ name = "oauthlib", specifier = "==3.3.1" },
{ name = "oci", specifier = "==2.169.0" },
{ name = "oci", specifier = "==2.183.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "openstacksdk", specifier = "==4.2.0" },
{ name = "opentelemetry-api", specifier = "==1.39.1" },
@@ -266,7 +266,7 @@ constraints = [
{ name = "pagerduty", specifier = "==6.1.0" },
{ name = "pandas", specifier = "==2.2.3" },
{ name = "pbr", specifier = "==7.0.3" },
{ name = "pillow", specifier = "==12.2.0" },
{ name = "pillow", specifier = "==12.3.0" },
{ name = "pkginfo", specifier = "==1.12.1.2" },
{ name = "platformdirs", specifier = "==4.5.1" },
{ name = "plotly", specifier = "==6.5.2" },
@@ -282,8 +282,8 @@ constraints = [
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "py-deviceid", specifier = "==0.1.1" },
{ name = "py-iam-expand", specifier = "==0.3.0" },
{ name = "py-ocsf-models", specifier = "==0.8.1" },
{ name = "pyasn1", specifier = "==0.6.3" },
{ name = "py-ocsf-models", specifier = "==0.10.0" },
{ name = "pyasn1", specifier = "==0.6.4" },
{ name = "pyasn1-modules", specifier = "==0.4.2" },
{ name = "pycodestyle", specifier = "==2.14.0" },
{ name = "pycparser", specifier = "==3.0" },
@@ -295,7 +295,7 @@ constraints = [
{ name = "pylint", specifier = "==3.2.5" },
{ name = "pymsalruntime", specifier = "==0.18.1" },
{ name = "pynacl", specifier = "==1.6.2" },
{ name = "pyopenssl", specifier = "==26.0.0" },
{ name = "pyopenssl", specifier = "==26.2.0" },
{ name = "pyparsing", specifier = "==3.3.2" },
{ name = "pyreadline3", specifier = "==3.5.4" },
{ name = "pysocks", specifier = "==1.7.1" },
@@ -364,7 +364,7 @@ constraints = [
{ name = "wcwidth", specifier = "==0.5.3" },
{ name = "websocket-client", specifier = "==1.9.0" },
{ name = "werkzeug", specifier = "==3.1.7" },
{ name = "workos", specifier = "==6.0.8" },
{ name = "workos", specifier = "==8.3.0" },
{ name = "wrapt", specifier = "==1.17.3" },
{ name = "xlsxwriter", specifier = "==3.2.9" },
{ name = "xmlsec", specifier = "==1.3.17" },
@@ -377,8 +377,15 @@ constraints = [
]
overrides = [
{ name = "azure-mgmt-containerservice", specifier = "==34.1.0" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "dulwich", specifier = "==1.2.5" },
{ name = "microsoft-kiota-abstractions", specifier = "==1.9.9" },
{ name = "microsoft-kiota-abstractions", specifier = "==1.9.10" },
{ name = "microsoft-kiota-authentication-azure", specifier = "==1.9.10" },
{ name = "microsoft-kiota-http", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-form", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-json", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.10" },
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" },
{ name = "okta", specifier = "==3.4.2" },
{ name = "pyjwt", extras = ["crypto"], specifier = "==2.13.0" },
]
@@ -853,7 +860,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0
[[package]]
name = "alibabacloud-tea-openapi"
version = "0.4.4"
version = "0.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alibabacloud-credentials" },
@@ -862,9 +869,9 @@ dependencies = [
{ name = "cryptography" },
{ name = "darabonba-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/93/138bcdc8fc596add73e37cf2073798f285284d1240bda9ee02f9384fc6be/alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce", size = 21960, upload-time = "2026-03-26T10:16:16.792Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/73/fb0c4d44759791ecdf269fc715c1e810fa1aba3981bfaaf8a01f61899296/alibabacloud_tea_openapi-0.4.5.tar.gz", hash = "sha256:75fa1f4360a46e41f5bf5f8d4917e52efb6f64885839bc1328c35590670c97b9", size = 26616, upload-time = "2026-07-14T13:15:39.364Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/5a/6bfc4506438c1809c486f66217ad11eab78157192b3d5707b4e2f4212f6c/alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f", size = 26236, upload-time = "2026-03-26T10:16:15.861Z" },
{ url = "https://files.pythonhosted.org/packages/8d/ec/6b368a10e9c2e8b1b394c69b96ac213ae66e8c4895e0baa1ffaf7178fd32/alibabacloud_tea_openapi-0.4.5-py3-none-any.whl", hash = "sha256:338979095c7beda80a5b413c31262892cafdc12069dde4ce4fc2e4f7ce0fc609", size = 33333, upload-time = "2026-07-14T13:15:38.365Z" },
]
[[package]]
@@ -2088,6 +2095,37 @@ toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "crc32c"
version = "2.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/66/7e97aa77af7cf6afbff26e3651b564fe41932599bc2d3dce0b2f73d4829a/crc32c-2.8.tar.gz", hash = "sha256:578728964e59c47c356aeeedee6220e021e124b9d3e8631d95d9a5e5f06e261c", size = 48179, upload-time = "2025-10-17T06:20:13.61Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/0b/5e03b22d913698e9cc563f39b9f6bbd508606bf6b8e9122cd6bf196b87ea/crc32c-2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e560a97fbb96c9897cb1d9b5076ef12fc12e2e25622530a1afd0de4240f17e1f", size = 66329, upload-time = "2025-10-17T06:19:01.771Z" },
{ url = "https://files.pythonhosted.org/packages/6b/38/2fe0051ffe8c6a650c8b1ac0da31b8802d1dbe5fa40a84e4b6b6f5583db5/crc32c-2.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6762d276d90331a490ef7e71ffee53b9c0eb053bd75a272d786f3b08d3fe3671", size = 62988, upload-time = "2025-10-17T06:19:02.953Z" },
{ url = "https://files.pythonhosted.org/packages/3e/30/5837a71c014be83aba1469c58820d287fc836512a0cad6b8fdd43868accd/crc32c-2.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:60670569f5ede91e39f48fb0cb4060e05b8d8704dd9e17ede930bf441b2f73ef", size = 61522, upload-time = "2025-10-17T06:19:03.796Z" },
{ url = "https://files.pythonhosted.org/packages/ca/29/63972fc1452778e2092ae998c50cbfc2fc93e3fa9798a0278650cd6169c5/crc32c-2.8-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:711743da6ccc70b3c6718c328947b0b6f34a1fe6a6c27cc6c1d69cc226bf70e9", size = 80200, upload-time = "2025-10-17T06:19:04.617Z" },
{ url = "https://files.pythonhosted.org/packages/cb/3a/60eb49d7bdada4122b3ffd45b0df54bdc1b8dd092cda4b069a287bdfcff4/crc32c-2.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eb4094a2054774f13b26f21bf56792bb44fa1fcee6c6ad099387a43ffbfb4fa", size = 81757, upload-time = "2025-10-17T06:19:05.496Z" },
{ url = "https://files.pythonhosted.org/packages/f5/63/6efc1b64429ef7d23bd58b75b7ac24d15df327e3ebbe9c247a0f7b1c2ed1/crc32c-2.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fff15bf2bd3e95780516baae935ed12be88deaa5ebe6143c53eb0d26a7bdc7b7", size = 80830, upload-time = "2025-10-17T06:19:06.621Z" },
{ url = "https://files.pythonhosted.org/packages/e1/eb/0ae9f436f8004f1c88f7429e659a7218a3879bd11a6b18ed1257aad7e98b/crc32c-2.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c0e11e3826668121fa53e0745635baf5e4f0ded437e8ff63ea56f38fc4f970a", size = 80095, upload-time = "2025-10-17T06:19:07.381Z" },
{ url = "https://files.pythonhosted.org/packages/9e/81/4afc9d468977a4cd94a2eb62908553345009a7c0d30e74463a15d4b48ec3/crc32c-2.8-cp311-cp311-win32.whl", hash = "sha256:38f915336715d1f1353ab07d7d786f8a789b119e273aea106ba55355dfc9101d", size = 64886, upload-time = "2025-10-17T06:19:08.497Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e8/94e839c9f7e767bf8479046a207afd440a08f5c59b52586e1af5e64fa4a0/crc32c-2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60e0a765b1caab8d31b2ea80840639253906a9351d4b861551c8c8625ea20f86", size = 66639, upload-time = "2025-10-17T06:19:09.338Z" },
{ url = "https://files.pythonhosted.org/packages/b6/36/fd18ef23c42926b79c7003e16cb0f79043b5b179c633521343d3b499e996/crc32c-2.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:572ffb1b78cce3d88e8d4143e154d31044a44be42cb3f6fbbf77f1e7a941c5ab", size = 66379, upload-time = "2025-10-17T06:19:10.115Z" },
{ url = "https://files.pythonhosted.org/packages/7f/b8/c584958e53f7798dd358f5bdb1bbfc97483134f053ee399d3eeb26cca075/crc32c-2.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cf827b3758ee0c4aacd21ceca0e2da83681f10295c38a10bfeb105f7d98f7a68", size = 63042, upload-time = "2025-10-17T06:19:10.946Z" },
{ url = "https://files.pythonhosted.org/packages/62/e6/6f2af0ec64a668a46c861e5bc778ea3ee42171fedfc5440f791f470fd783/crc32c-2.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:106fbd79013e06fa92bc3b51031694fcc1249811ed4364ef1554ee3dd2c7f5a2", size = 61528, upload-time = "2025-10-17T06:19:11.768Z" },
{ url = "https://files.pythonhosted.org/packages/17/8b/4a04bd80a024f1a23978f19ae99407783e06549e361ab56e9c08bba3c1d3/crc32c-2.8-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6dde035f91ffbfe23163e68605ee5a4bb8ceebd71ed54bb1fb1d0526cdd125a2", size = 80028, upload-time = "2025-10-17T06:19:12.554Z" },
{ url = "https://files.pythonhosted.org/packages/21/8f/01c7afdc76ac2007d0e6a98e7300b4470b170480f8188475b597d1f4b4c6/crc32c-2.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e41ebe7c2f0fdcd9f3a3fd206989a36b460b4d3f24816d53e5be6c7dba72c5e1", size = 81531, upload-time = "2025-10-17T06:19:13.406Z" },
{ url = "https://files.pythonhosted.org/packages/32/2b/8f78c5a8cc66486be5f51b6f038fc347c3ba748d3ea68be17a014283c331/crc32c-2.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecf66cf90266d9c15cea597d5cc86c01917cd1a238dc3c51420c7886fa750d7e", size = 80608, upload-time = "2025-10-17T06:19:14.223Z" },
{ url = "https://files.pythonhosted.org/packages/db/86/fad1a94cdeeeb6b6e2323c87f970186e74bfd6fbfbc247bf5c88ad0873d5/crc32c-2.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59eee5f3a69ad0793d5fa9cdc9b9d743b0cd50edf7fccc0a3988a821fef0208c", size = 79886, upload-time = "2025-10-17T06:19:15.345Z" },
{ url = "https://files.pythonhosted.org/packages/d5/db/1a7cb6757a1e32376fa2dfce00c815ea4ee614a94f9bff8228e37420c183/crc32c-2.8-cp312-cp312-win32.whl", hash = "sha256:a73d03ce3604aa5d7a2698e9057a0eef69f529c46497b27ee1c38158e90ceb76", size = 64896, upload-time = "2025-10-17T06:19:16.457Z" },
{ url = "https://files.pythonhosted.org/packages/bf/8e/2024de34399b2e401a37dcb54b224b56c747b0dc46de4966886827b4d370/crc32c-2.8-cp312-cp312-win_amd64.whl", hash = "sha256:56b3b7d015247962cf58186e06d18c3d75a1a63d709d3233509e1c50a2d36aa2", size = 66645, upload-time = "2025-10-17T06:19:17.235Z" },
{ url = "https://files.pythonhosted.org/packages/a7/1d/dd926c68eb8aac8b142a1a10b8eb62d95212c1cf81775644373fe7cceac2/crc32c-2.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5833f4071da7ea182c514ba17d1eee8aec3c5be927d798222fbfbbd0f5eea02c", size = 62345, upload-time = "2025-10-17T06:20:09.39Z" },
{ url = "https://files.pythonhosted.org/packages/51/be/803404e5abea2ef2c15042edca04bbb7f625044cca879e47f186b43887c2/crc32c-2.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1dc4da036126ac07b39dd9d03e93e585ec615a2ad28ff12757aef7de175295a8", size = 61229, upload-time = "2025-10-17T06:20:10.236Z" },
{ url = "https://files.pythonhosted.org/packages/fc/3a/00cc578cd27ed0b22c9be25cef2c24539d92df9fa80ebd67a3fc5419724c/crc32c-2.8-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15905fa78344654e241371c47e6ed2411f9eeb2b8095311c68c88eccf541e8b4", size = 64108, upload-time = "2025-10-17T06:20:11.072Z" },
{ url = "https://files.pythonhosted.org/packages/6b/bc/0587ef99a1c7629f95dd0c9d4f3d894de383a0df85831eb16c48a6afdae4/crc32c-2.8-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c596f918688821f796434e89b431b1698396c38bf0b56de873621528fe3ecb1e", size = 64815, upload-time = "2025-10-17T06:20:11.919Z" },
{ url = "https://files.pythonhosted.org/packages/73/42/94f2b8b92eae9064fcfb8deef2b971514065bd606231f8857ff8ae02bebd/crc32c-2.8-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8d23c4fe01b3844cb6e091044bc1cebdef7d16472e058ce12d9fadf10d2614af", size = 66659, upload-time = "2025-10-17T06:20:12.766Z" },
]
[[package]]
name = "cron-descriptor"
version = "1.4.5"
@@ -2112,47 +2150,45 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.7"
version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
{ url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
{ url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
{ url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
{ url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
{ url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
{ url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
{ url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
{ url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
{ url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
{ url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
]
[[package]]
@@ -2166,15 +2202,17 @@ wheels = [
[[package]]
name = "darabonba-core"
version = "1.0.5"
version = "1.0.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "alibabacloud-tea" },
{ name = "requests" },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/83/9321ccdb7a800c2cb97d8fa34bead5f20141f27f804594fd1fd815c4cd07/darabonba_core-1.0.8.tar.gz", hash = "sha256:f1661960b368e342d3d36434be82d264b70a01c49e843921d8a4dacd217376ae", size = 27604, upload-time = "2026-07-13T02:07:34.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/d3/a7daaee544c904548e665829b51a9fa2572acb82c73ad787a8ff90273002/darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c", size = 24580, upload-time = "2025-12-12T07:53:59.494Z" },
{ url = "https://files.pythonhosted.org/packages/6d/88/38800ca22f39a31fdb75c7b2867c61d3af5e2792cee0b72942a639c88a79/darabonba_core-1.0.8-py3-none-any.whl", hash = "sha256:ac093fdd40f88f2f9dfbbbfd7bc143495a3cb031f35b397c98d24edfa6b69483", size = 30957, upload-time = "2026-07-13T02:07:33.138Z" },
]
[[package]]
@@ -3327,14 +3365,14 @@ wheels = [
[[package]]
name = "httplib2"
version = "0.31.2"
version = "0.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" },
{ url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" },
]
[[package]]
@@ -3357,6 +3395,134 @@ http2 = [
{ name = "h2" },
]
[[package]]
name = "huaweicloudsdkcore"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "defusedxml" },
{ name = "pyasn1" },
{ name = "pymongo" },
{ name = "pyyaml" },
{ name = "requests-toolbelt" },
{ name = "simplejson" },
{ name = "six" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/f5/65e90764ea3bbfef50fb68cd5e12340acf1f51e9276b11745fbf5feb7e0e/huaweicloudsdkcore-3.1.204-py3-none-any.whl", hash = "sha256:9ae17744795ebdc8ce9291373a3a27bf72e90aa98677cfce0ea9394376875a95", size = 69578, upload-time = "2026-07-09T09:01:59.715Z" },
]
[[package]]
name = "huaweicloudsdkcts"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/32/d06328e35375d4aa606719a27856cdf57b1f7fb0c49d4dfd22a9609dba19/huaweicloudsdkcts-3.1.204-py3-none-any.whl", hash = "sha256:9def561aa784a6ee13b46bfc96888cd1df5bfc42f8a89e60b42c91c608bf6d60", size = 121768, upload-time = "2026-07-09T09:02:08.16Z" },
]
[[package]]
name = "huaweicloudsdkecs"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/66/f8e4a3b9ca70d3ea79c4d200f928ed9ffdf4910ac01be4864967408c8f18/huaweicloudsdkecs-3.1.204-py3-none-any.whl", hash = "sha256:dc5715d782c0260b901c793d009d5e632257acb04257b6f2c6631e415c589343", size = 765699, upload-time = "2026-07-09T09:02:39.272Z" },
]
[[package]]
name = "huaweicloudsdkelb"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/4b/9bdc7e2066419d967e9b812cfacfb9c4996a8e977dc39853309a3c1ac9e2/huaweicloudsdkelb-3.1.204-py3-none-any.whl", hash = "sha256:620247c2b2a7f20e7da8b18fe9c64e29972055f015bc35270fb5b43243dc4830", size = 1292397, upload-time = "2026-07-09T09:02:45.656Z" },
]
[[package]]
name = "huaweicloudsdkevs"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/cf/531dc55fd9d0f3bbd3eef24c7e4d78c6a1ba8eb80fa506e3574d72bcc98a/huaweicloudsdkevs-3.1.204-py3-none-any.whl", hash = "sha256:9118ac4c576e54aa7eaa926949e2b6824c5f038a2274b51d9a304d37fc0d7e2f", size = 251404, upload-time = "2026-07-09T09:02:50.05Z" },
]
[[package]]
name = "huaweicloudsdkiam"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/9a/7da0fbe9b83bc7a7f6d586366b81e829ed355a57419d2184b7dd51f8c2a3/huaweicloudsdkiam-3.1.204-py3-none-any.whl", hash = "sha256:0021e204f81ceef2640017e517adb72ba56c9ced03f071a0265b10bc9759badf", size = 1251350, upload-time = "2026-07-09T09:03:05.467Z" },
]
[[package]]
name = "huaweicloudsdkkms"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/3a/7392617d585cb2005f7d9ade0b0e0e493a7a88daf56e8dc39e4e219cc5d0/huaweicloudsdkkms-3.1.204-py3-none-any.whl", hash = "sha256:378986f33113ce99f445ef318d1c7dda89e361d16c008e5ef9793981d8385376", size = 275690, upload-time = "2026-07-09T09:03:29.833Z" },
]
[[package]]
name = "huaweicloudsdkobs"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/49/28a09e1e33d1c039be22ee4171efaa739351653c7aa88d3a2f7a78d90217/huaweicloudsdkobs-3.1.204-py3-none-any.whl", hash = "sha256:8c5830fa30293185964d98e524887fc510c8e17ca2fadb4563dad10910f37b13", size = 235360, upload-time = "2026-07-09T09:03:52.171Z" },
]
[[package]]
name = "huaweicloudsdkrds"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/7d/721f162c46e3de604a73674223bf6c6bc6cf7ade25b3751a71288f4dd122/huaweicloudsdkrds-3.1.204-py3-none-any.whl", hash = "sha256:a790b5b3c457a608e5679c101f463b4d037dd9a8a66f6e46144a9e5a4b37780f", size = 1626906, upload-time = "2026-07-09T09:04:06.936Z" },
]
[[package]]
name = "huaweicloudsdkvpc"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/b5/4baa27c3a275ea92806068e35e06f249a30add8dd57c777bb45841f63406/huaweicloudsdkvpc-3.1.204-py3-none-any.whl", hash = "sha256:c57d6b6d2f70deca91e86f7956b33fc9ac4991b431f0d608c3632231119f8970", size = 1124332, upload-time = "2026-07-09T09:04:39.797Z" },
]
[[package]]
name = "huaweicloudsdkwaf"
version = "3.1.204"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huaweicloudsdkcore" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/21/01590dce200be487756451f5e9efb99da7810688062d177e4b58d6062465/huaweicloudsdkwaf-3.1.204-py3-none-any.whl", hash = "sha256:b2355276e0029808f45e2d1bd3eb14b37d2da9417e61e642ffe0e2b748ca8283", size = 1337762, upload-time = "2026-07-09T09:04:43.688Z" },
]
[[package]]
name = "humanfriendly"
version = "10.0"
@@ -3920,21 +4086,21 @@ wheels = [
[[package]]
name = "microsoft-kiota-abstractions"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
{ name = "std-uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/94/37315b82a1bcc08145e5bc2af7396a4be8160ac138ec269611c3b9589b7a/microsoft_kiota_abstractions-1.9.9.tar.gz", hash = "sha256:5df9a8e0517a4568726c2cac6d9789284cc6ffa66043b68eba42ae55749fb861", size = 24468, upload-time = "2026-03-02T21:03:50.133Z" }
sdist = { url = "https://files.pythonhosted.org/packages/45/e1/39de28380fc0eddf12f66099469fb7561bc38f577ea06e3a074751ebbcd9/microsoft_kiota_abstractions-1.9.10.tar.gz", hash = "sha256:8eb62d64c35ad0eeb4e8bcdbb143c0b308dc4a494e757f8e44cb959d34f44ecf", size = 24473, upload-time = "2026-03-12T17:27:15.398Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/6a/7d5a1a8131f0eccc6b45839c091aa00ba29661854e7defaa7936cf342fa7/microsoft_kiota_abstractions-1.9.9-py3-none-any.whl", hash = "sha256:8d0a14eda42f3f0ccac2e9512227a338f69998dc9b782fd21cb8ca7c48302caa", size = 44453, upload-time = "2026-03-02T21:03:51.11Z" },
{ url = "https://files.pythonhosted.org/packages/4d/59/bf0cb26c80fbd3fa882df8474ad87e9dbd742656c376388c427c4e314171/microsoft_kiota_abstractions-1.9.10-py3-none-any.whl", hash = "sha256:cd169067ebe48e6feea1258630807034239e0c61c2abe5fd66896a58177e8f05", size = 44462, upload-time = "2026-03-12T17:27:16.532Z" },
]
[[package]]
name = "microsoft-kiota-authentication-azure"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -3943,14 +4109,14 @@ dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ca/ce/5ae8b37ee4a50f0ed5e092c2d0105d60b592e6102a190959f76658a0994c/microsoft_kiota_authentication_azure-1.9.9.tar.gz", hash = "sha256:aca5e7dc8a0a28224f9025a479349ac2f9aaf166bfd6bc707f232658b45eec28", size = 5000, upload-time = "2026-03-02T21:04:02.355Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/53/7760f979c141ec590f0c1cfcb92b3e410eb2909cc19feb42f3fce78db171/microsoft_kiota_authentication_azure-1.9.10.tar.gz", hash = "sha256:b9f10a9fa86e36114abfee448d2dab91a502d6a55d349a306e2e41a1218fe1ad", size = 4999, upload-time = "2026-03-12T17:27:26.323Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/de/dc504324b776d00a420886cc6f39e04be2cf48cab0e9b18f8450a5efcc29/microsoft_kiota_authentication_azure-1.9.9-py3-none-any.whl", hash = "sha256:73dc21a1a2861ea78a135327291db3322e2255542a18b311dd03fd908342e902", size = 6951, upload-time = "2026-03-02T21:04:03.18Z" },
{ url = "https://files.pythonhosted.org/packages/1e/4a/e7852f9358d897ada1eec4e825c815761befe36df4defa79f1ae6c7b588c/microsoft_kiota_authentication_azure-1.9.10-py3-none-any.whl", hash = "sha256:b5d98b0d17173c61c0c7ab4274ea4ca69253b3c13424137758034506694964e9", size = 6961, upload-time = "2026-03-12T17:27:27.238Z" },
]
[[package]]
name = "microsoft-kiota-http"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx", extra = ["http2"] },
@@ -3958,57 +4124,57 @@ dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/3f/fc18eb0d1d845daf6355fd54fd990af7f7e10043ef6a6da39b9e5981cbaf/microsoft_kiota_http-1.9.9.tar.gz", hash = "sha256:ae672b145df71b644f8da0951767a12a4ce47a40576d86eba19b7c22d9e160f9", size = 21493, upload-time = "2026-03-02T21:04:11.662Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/e5/20972b620bd8cca086c284e97b285d437c108a23fee122ad7b92bd246c1a/microsoft_kiota_http-1.9.10.tar.gz", hash = "sha256:af1838d091f76426c974897357093ed977ce66f1d808cb161c190de873bb5833", size = 21493, upload-time = "2026-03-12T17:27:35.393Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/6a/cc1b1055b4b6d4dfc1be7a71917c2f0ef19c070c6a18b16d3c1032d20925/microsoft_kiota_http-1.9.9-py3-none-any.whl", hash = "sha256:a5b1b217ac9afeb4054f12515417e3b1d2be12a9385a70a41d18d64379ea2e7e", size = 31945, upload-time = "2026-03-02T21:04:12.328Z" },
{ url = "https://files.pythonhosted.org/packages/ce/f4/78ce18330a626138b2ff6bb62574adac01e8b9ee87c1349ddfeb9cab0556/microsoft_kiota_http-1.9.10-py3-none-any.whl", hash = "sha256:6127032c8d94f8607e4d36d0822b88bc8689ab368b4c00d6c7beb7d2d0f2ab10", size = 31960, upload-time = "2026-03-12T17:27:36.1Z" },
]
[[package]]
name = "microsoft-kiota-serialization-form"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ee/b4/18e9fce60a30c8b6ea0a6278fb81cf352127340d48df2d7c52ff1b579488/microsoft_kiota_serialization_form-1.9.9.tar.gz", hash = "sha256:3cdc8b172baec5b5282af72f2ce02715edcd23252ce0b5af96075256edd75114", size = 9015, upload-time = "2026-03-02T21:04:20.39Z" }
sdist = { url = "https://files.pythonhosted.org/packages/56/90/7e1a090a2099acae1a1baa9a0762214b73b63d9268369b510994f75f54e4/microsoft_kiota_serialization_form-1.9.10.tar.gz", hash = "sha256:4c6655d8cd479d1ada63fdfe6a272e50d87d7c8369dbc8e13833ba4787fc798b", size = 9012, upload-time = "2026-03-12T17:27:44.214Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/24/eb8436b882f1473bd0a868848d214df3df2d9b3db8e5422d111032f1114f/microsoft_kiota_serialization_form-1.9.9-py3-none-any.whl", hash = "sha256:1c426d4f0d463fc9215c41d7fa0f3dc5fe8d3c80573d555cf63ea67000148d84", size = 10718, upload-time = "2026-03-02T21:04:21.25Z" },
{ url = "https://files.pythonhosted.org/packages/d9/4c/5092fc896b34c21e8b9c03c63006b313a81e2377176a69c97aa6a9c8f5bb/microsoft_kiota_serialization_form-1.9.10-py3-none-any.whl", hash = "sha256:765d3f6408668f58bfdf892c32b45967c579d9131f3ba5a6b6868cb7ab956bfe", size = 10728, upload-time = "2026-03-12T17:27:45.103Z" },
]
[[package]]
name = "microsoft-kiota-serialization-json"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b8/2f/d36eba916c00136da122d1701acb862c5b1f2e22b6dc6fa4e0f4abda2786/microsoft_kiota_serialization_json-1.9.9.tar.gz", hash = "sha256:9b27479427f49bbac15ead8e8ff0176e47fcdf81153611acc408f5f399342079", size = 9545, upload-time = "2026-03-02T21:04:29.177Z" }
sdist = { url = "https://files.pythonhosted.org/packages/67/0e/55afd533a764ba77da988b7ca4242c84867a3a25f2ff0bf4c2b24b5e8fca/microsoft_kiota_serialization_json-1.9.10.tar.gz", hash = "sha256:6063028f30dd67afa2db20a72d9bde5e5d26d468f8bdedadd1445cf7c7630e17", size = 9746, upload-time = "2026-03-12T17:27:53.015Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/7b/b3f606ef2dcbdebe12ae27004ed6e7542370cb2494265f11a8877a1de2d1/microsoft_kiota_serialization_json-1.9.9-py3-none-any.whl", hash = "sha256:bb80b93e81bab41dc142e9b254f79bf0b7b9fe49a796ca0c8e8691925bd3967f", size = 11210, upload-time = "2026-03-02T21:04:29.844Z" },
{ url = "https://files.pythonhosted.org/packages/2f/56/d14c0185c8092abde1a60ad2bdd4480bb2ddb551ce71c6de1e6133a4d8d1/microsoft_kiota_serialization_json-1.9.10-py3-none-any.whl", hash = "sha256:0545ae910160b19caaa8c30c90c7416e1966294fbd6cc5af01f0e116a18f223a", size = 11452, upload-time = "2026-03-12T17:27:53.909Z" },
]
[[package]]
name = "microsoft-kiota-serialization-multipart"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/44/24087f0fac7c5682c13c7fb61468a0c5a5185b9f243de3a99309aa6fcaa7/microsoft_kiota_serialization_multipart-1.9.9.tar.gz", hash = "sha256:f8730be6da5f6c63a6bf4ea310a9723b9998a47a04745887dc156d08f119a829", size = 5162, upload-time = "2026-03-02T21:04:48.1Z" }
sdist = { url = "https://files.pythonhosted.org/packages/24/34/eadc15c2a3131e2a76126f3112c32b73502cb5a335e2e40cac2877e5d843/microsoft_kiota_serialization_multipart-1.9.10.tar.gz", hash = "sha256:8f2da4f93e79b09f9738b6889685e47acfafcca870db94ab1d4cd233d69e4268", size = 5167, upload-time = "2026-03-12T17:28:18.507Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/db/6b988fdf771c3d07dff4a116176d575832daf2a43823444d145d71da5b61/microsoft_kiota_serialization_multipart-1.9.9-py3-none-any.whl", hash = "sha256:572e9cbafa2eb946452cdadfb019a4e9245768c0d61c3089d3436d4f5106c550", size = 6696, upload-time = "2026-03-02T21:04:48.98Z" },
{ url = "https://files.pythonhosted.org/packages/fa/40/345cbcee6c52b4261fedf4ae2ff8573aec47ce4ae2015ea8b57c75ef978b/microsoft_kiota_serialization_multipart-1.9.10-py3-none-any.whl", hash = "sha256:7cadc26483b567c738f926b044521569e0b797446053c9e8eab02269d4a81062", size = 6708, upload-time = "2026-03-12T17:28:19.397Z" },
]
[[package]]
name = "microsoft-kiota-serialization-text"
version = "1.9.9"
version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/3c/d244ad08e03003134871698aa54de8243bcc61c0faf3ab114293bb76d6ad/microsoft_kiota_serialization_text-1.9.9.tar.gz", hash = "sha256:18bc0764dda4078a4c953300253344e05d0cdb9c17136f1a2f695d438cedb402", size = 7325, upload-time = "2026-03-02T21:04:37.567Z" }
sdist = { url = "https://files.pythonhosted.org/packages/74/a6/28a4a8d5c01f08e363135fc9585cab3c02d1b1a69c3c16032e6abb35dfed/microsoft_kiota_serialization_text-1.9.10.tar.gz", hash = "sha256:cfc433c2a95ea3c3ec43c8b09002fbf65c998c5c0571205df161fe0e9d5d8de7", size = 7326, upload-time = "2026-03-12T17:28:01.621Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/f8/43f8d00fed6e090810d3ce0c05e06c23eaa5dee6e87ab1fb89d96ca9559f/microsoft_kiota_serialization_text-1.9.9-py3-none-any.whl", hash = "sha256:84418119d4929a76fde7f31e957e240e003bf145757838b9aa3a0f36dec1b789", size = 8885, upload-time = "2026-03-02T21:04:38.76Z" },
{ url = "https://files.pythonhosted.org/packages/dd/bf/dd36e4a6d1cff3f2d30f03e2479cd38210e32d4715bb6a9f0e2737f13604/microsoft_kiota_serialization_text-1.9.10-py3-none-any.whl", hash = "sha256:742890cfd4450d12f58d42da7cfa474fe1ee5d6442e016bf70ab76e5c876c0ea", size = 8896, upload-time = "2026-03-12T17:28:02.328Z" },
]
[[package]]
@@ -4260,20 +4426,22 @@ wheels = [
[[package]]
name = "oci"
version = "2.169.0"
version = "2.183.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "circuitbreaker" },
{ name = "crc32c" },
{ name = "cryptography" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "pyopenssl" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/f4/3c2eddccc75dd06a692dbb3290f20f4bc733d99dc60de21f22d65efdeae4/oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa", size = 16885837, upload-time = "2026-03-31T06:14:58.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/2a/77bd6cbf1c69b2f368fe3d6462d84369b0cba15e37ce713cdc08d459b95a/oci-2.183.0.tar.gz", hash = "sha256:ff572ef5f2030a788796bb509d257e6a41c6510ef9b4b6a75a079efd06e533ce", size = 17759723, upload-time = "2026-07-28T06:02:29.76Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/bf/19643bd939ab595193779ee25c2c12aef8e9a54e0a68de5ed79f209702e3/oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b", size = 34460447, upload-time = "2026-03-31T06:14:51.373Z" },
{ url = "https://files.pythonhosted.org/packages/a9/de/8574b3e527996a099d196e87794a4652d91a0c3185fcc7fdbb5649b75a8a/oci-2.183.0-py3-none-any.whl", hash = "sha256:bd789c98a94d7c5ea08c20d11dcf68c9cd1ad479b134727d80a930b84387070b", size = 36133501, upload-time = "2026-07-28T06:02:18.239Z" },
]
[[package]]
@@ -4467,39 +4635,33 @@ wheels = [
[[package]]
name = "pillow"
version = "12.2.0"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
{ url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
{ url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
{ url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
{ url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
{ url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
{ url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
{ url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
{ url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
{ url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
{ url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
{ url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
{ url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
{ url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
{ url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[package]]
@@ -4673,8 +4835,8 @@ wheels = [
[[package]]
name = "prowler"
version = "5.35.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#f5ea116763aeffede9f399c8934fc280eaccd315" }
version = "5.37.1"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.37#fbbf9fe1cdfbac1869b950a078ac36098dbb4ddc" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4729,6 +4891,17 @@ dependencies = [
{ name = "google-api-python-client" },
{ name = "google-auth-httplib2" },
{ name = "h2" },
{ name = "huaweicloudsdkcore" },
{ name = "huaweicloudsdkcts" },
{ name = "huaweicloudsdkecs" },
{ name = "huaweicloudsdkelb" },
{ name = "huaweicloudsdkevs" },
{ name = "huaweicloudsdkiam" },
{ name = "huaweicloudsdkkms" },
{ name = "huaweicloudsdkobs" },
{ name = "huaweicloudsdkrds" },
{ name = "huaweicloudsdkvpc" },
{ name = "huaweicloudsdkwaf" },
{ name = "jsonschema" },
{ name = "kingfisher-bin" },
{ name = "kubernetes" },
@@ -4762,7 +4935,7 @@ dependencies = [
[[package]]
name = "prowler-api"
version = "1.38.0"
version = "1.38.2"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -4862,7 +5035,7 @@ requires-dist = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.37" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
@@ -4978,25 +5151,24 @@ wheels = [
[[package]]
name = "py-ocsf-models"
version = "0.8.1"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "email-validator" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/70/61e2f9ce3d7e83aa5339ed6ae17e473c15c7a36f161c6dbea0e939e3af0c/py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c", size = 36540, upload-time = "2026-02-12T16:50:15.233Z" }
sdist = { url = "https://files.pythonhosted.org/packages/70/d6/f0787cbe953e3cf6ef4430f3cc7d66cbbaabe4b20cb82cc27cc2d21e622a/py_ocsf_models-0.10.0.tar.gz", hash = "sha256:29abaa5a3d4ebba0e2a21757508a4848fa5e1d57da233af57e580f97f0223c59", size = 36498, upload-time = "2026-07-13T07:05:44.448Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/18/63790884bf33f820e2c60f8d5038b5d6de967a03343ddf237c054e1d6d08/py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e", size = 64354, upload-time = "2026-02-12T16:50:12.983Z" },
{ url = "https://files.pythonhosted.org/packages/75/56/eca45ec87a02f930cc7eaa7cb36660f69fb00c3d77bb4a84bb92d6c94c25/py_ocsf_models-0.10.0-py3-none-any.whl", hash = "sha256:a9d1e245b1c9fba1d2cb8c042253ef1b83a2dbfec30ed69975bbce599b4510bb", size = 64334, upload-time = "2026-07-13T07:05:42.93Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.3"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
@@ -5187,6 +5359,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/ff/7f52c1461d8ceaefa989d2700a027f84427879bb7571145bbffdec5d5f4a/pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6", size = 519603, upload-time = "2024-06-28T13:10:23.526Z" },
]
[[package]]
name = "pymongo"
version = "4.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c0c6732fbd358b75a07e17d7e588fd23d481b9812ca96ceeff90bbf879fc/pymongo-4.15.1.tar.gz", hash = "sha256:b9f379a4333dc3779a6bf7adfd077d4387404ed1561472743486a9c58286f705", size = 2470613, upload-time = "2025-09-16T16:39:47.24Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/da/89066930a70b4299844f1155fc23baaa7e30e77c8a0cbf62a2ae06ee34a5/pymongo-4.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:363445cc0e899b9e55ac9904a868c8a16a6c81f71c48dbadfd78c98e0b54de27", size = 865410, upload-time = "2025-09-16T16:38:16.279Z" },
{ url = "https://files.pythonhosted.org/packages/99/8f/a1d0402d52e5ebd14283718abefdc0c16f308cf10bee56cdff04b1f5119b/pymongo-4.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da0a13f345f4b101776dbab92cec66f0b75015df0b007b47bd73bfd0305cc56a", size = 865695, upload-time = "2025-09-16T16:38:18.015Z" },
{ url = "https://files.pythonhosted.org/packages/53/38/d1ef69028923f86fd00638d9eb16400d4e60a89eabd2011fe631fd3186cf/pymongo-4.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9481a492851e432122a83755d4e69c06aeb087bbf8370bac9f96d112ac1303fd", size = 1434758, upload-time = "2025-09-16T16:38:20.141Z" },
{ url = "https://files.pythonhosted.org/packages/b0/eb/a8d5dff748a2dd333610b2e4c8120b623e38ea2b5e30ad190d0ce2803840/pymongo-4.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625dec3e9cd7c3d336285a20728c01bfc56d37230a99ec537a6a8625af783a43", size = 1485716, upload-time = "2025-09-16T16:38:21.607Z" },
{ url = "https://files.pythonhosted.org/packages/c4/d4/17ba457a828b733182ddc01a202872fef3006eed6b54450b20dc95a2f77d/pymongo-4.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a31af455bffcc64537a7f67e2f84833a57855a82d05a085a1030c471138990", size = 1460160, upload-time = "2025-09-16T16:38:23.509Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/42b8662c09f5ca9c81d18d160f48e58842e0fa4c314ea02613c5e5d54542/pymongo-4.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4415970d2a074d5890696af10e174d84cb735f1fa7673020c7538431e1cb6e", size = 1439284, upload-time = "2025-09-16T16:38:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/b3/bb/46b9d978161828eb91973bd441a3f05f73c789203e976332a8de2832d5db/pymongo-4.15.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ee050a2e026e2b224d2ed382830194be20a81c78e1ef98f467e469071df3ac", size = 1407933, upload-time = "2025-09-16T16:38:27.045Z" },
{ url = "https://files.pythonhosted.org/packages/4b/55/bd5af98f675001f4b06f7314b3918e45809424a7ad3510f823f6703cd8f2/pymongo-4.15.1-cp311-cp311-win32.whl", hash = "sha256:9aef07d33839f6429dc24f2ef36e4ec906979cb4f628c57a1c2676cc66625711", size = 844328, upload-time = "2025-09-16T16:38:28.513Z" },
{ url = "https://files.pythonhosted.org/packages/c3/78/90989a290dd458ed43a8a04fa561ac9c7b3391f395cdacd42e21f0f22ce4/pymongo-4.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6e5ff4d6747e7b64966629a964db3089e9c1e0206d8f9cc8720c90f5a7af1", size = 858951, upload-time = "2025-09-16T16:38:30.074Z" },
{ url = "https://files.pythonhosted.org/packages/de/bb/d4d23f06e166cd773f2324cff73841a62d78a1ad16fb799cf7c5490ce32c/pymongo-4.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:bb783d9001b464a6ef3ee76c30ebbb6f977caee7bbc3a9bb1bd2ff596e818c46", size = 848290, upload-time = "2025-09-16T16:38:31.741Z" },
{ url = "https://files.pythonhosted.org/packages/7e/31/bc4525312083706a59fffe6e8de868054472308230fdee8db0c452c2b831/pymongo-4.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bab357c5ff36ba2340dfc94f3338ef399032089d35c3d257ce0c48630b7848b2", size = 920261, upload-time = "2025-09-16T16:38:33.614Z" },
{ url = "https://files.pythonhosted.org/packages/ae/55/4d99aec625494f21151b8b31e12e06b8ccd3b9dcff609b0dd1acf9bbbc0e/pymongo-4.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46d1af3eb2c274f07815372b5a68f99ecd48750e8ab54d5c3ff36a280fb41c8e", size = 919956, upload-time = "2025-09-16T16:38:35.121Z" },
{ url = "https://files.pythonhosted.org/packages/be/60/8f1afa41521df950e13f6490ecdef48155fc63b78f926e7649045e07afd1/pymongo-4.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc31357379318881186213dc5fc49b62601c955504f65c8e72032b5048950a1", size = 1698596, upload-time = "2025-09-16T16:38:36.586Z" },
{ url = "https://files.pythonhosted.org/packages/bc/3f/e48d50ee8d6aa0a4cda7889dd73076ec2ab79a232716a5eb0b9df070ffcf/pymongo-4.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12140d29da1ecbaefee2a9e65433ef15d6c2c38f97bc6dab0ff246a96f9d20cd", size = 1762833, upload-time = "2025-09-16T16:38:38.09Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/db976859efc617f608754e051e1468459d9a818fe1ad5d0862e8af57720b/pymongo-4.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf193d2dcd91fa1d1dfa1fd036a3b54f792915a4842d323c0548d23d30461b59", size = 1731875, upload-time = "2025-09-16T16:38:39.742Z" },
{ url = "https://files.pythonhosted.org/packages/18/59/3643ad52a5064ad3ef8c32910de6da28eb658234c25f2db5366f16bffbfb/pymongo-4.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c0bdcf4d57e4861ed323ba430b585ad98c010a83e46cb8aa3b29c248a82be1", size = 1701853, upload-time = "2025-09-16T16:38:41.333Z" },
{ url = "https://files.pythonhosted.org/packages/d8/96/441c190823f855fc6445ea574b39dca41156acf723c5e6a69ee718421700/pymongo-4.15.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43fcfc19446e0706bbfe86f683a477d1e699b02369dd9c114ec17c7182d1fe2b", size = 1660978, upload-time = "2025-09-16T16:38:42.877Z" },
{ url = "https://files.pythonhosted.org/packages/47/49/bd7e783fb78aaf9bdaa3f88cc238449be5bc5546e930ec98845ef235f809/pymongo-4.15.1-cp312-cp312-win32.whl", hash = "sha256:e5fedea0e7b3747da836cd5f88b0fa3e2ec5a394371f9b6a6b15927cfeb5455d", size = 891175, upload-time = "2025-09-16T16:38:44.658Z" },
{ url = "https://files.pythonhosted.org/packages/2e/28/7de5858bdeaa07ea4b277f9eb06123ea358003659fe55e72e4e7c898b321/pymongo-4.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:330a17c1c89e2c3bf03ed391108f928d5881298c17692199d3e0cdf097a20082", size = 910619, upload-time = "2025-09-16T16:38:46.124Z" },
{ url = "https://files.pythonhosted.org/packages/17/87/c39f4f8415e7c65f8b66413f53a9272211ff7dfe78a5128b27027bf88864/pymongo-4.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:756b7a2a80ec3dd5b89cd62e9d13c573afd456452a53d05663e8ad0c5ff6632b", size = 896229, upload-time = "2025-09-16T16:38:48.563Z" },
]
[[package]]
name = "pymsalruntime"
version = "0.18.1"
@@ -5223,15 +5426,15 @@ wheels = [
[[package]]
name = "pyopenssl"
version = "26.0.0"
version = "26.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" },
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
]
[[package]]
@@ -5556,6 +5759,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]
[[package]]
name = "requestsexceptions"
version = "1.4.0"
@@ -5774,6 +5989,37 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/06/c6dcc975a1e7d89bc764fd271da8138b318e18080b48e7f1acd2ab63df28/shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6", size = 57939, upload-time = "2023-12-17T01:42:02.426Z" }
[[package]]
name = "simplejson"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0e/2a/54837395a3487c725669428d513293612a48d82b95a0642c936932e5d898/simplejson-4.1.1.tar.gz", hash = "sha256:c08eb9f7a90f77ae470e19a07472e9a79ebc0d1c2315d86a72767665bd5ba79f", size = 118860, upload-time = "2026-04-24T19:24:59.819Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/25/39013ffe279d90093ec1c848565b3683c586906c10fa55d9000ec29d046b/simplejson-4.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2867c64d92abd1992c15666fae198203093f593e43d6b81adf176bae530d493a", size = 111538, upload-time = "2026-04-24T19:22:49.051Z" },
{ url = "https://files.pythonhosted.org/packages/f2/ae/2c272971c8a87e2539c54a98eb6ff037bee1e2e93943c3986cf7500a4f3a/simplejson-4.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c47c46e16c8ea9e4850061e6ed5aa2b9cd2074cb2274bfd9c138cba15ce7453", size = 90594, upload-time = "2026-04-24T19:22:50.408Z" },
{ url = "https://files.pythonhosted.org/packages/4e/a2/6eebfb99dedc139f549200f61ade6d1890ac5707c5d427bdfa6fe39c9313/simplejson-4.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e294e33dbf316a9bbdd4030d46503c9b0f19470ae7ad6af5bae6c426bc2e869f", size = 90718, upload-time = "2026-04-24T19:22:51.694Z" },
{ url = "https://files.pythonhosted.org/packages/80/7e/c9e6c0c4ad8415e64dad0c47f619b556b02680a41631b4dbc281d55dc54d/simplejson-4.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ce252b28fddbdd83db5bd7d93dad2a8a591d7ada098afec9c1b23d6b722a7a4", size = 180901, upload-time = "2026-04-24T19:22:53.025Z" },
{ url = "https://files.pythonhosted.org/packages/34/09/69e331e3994b1ed9be6ce9ace4ade704e7ed503edf869929ca7bb404eda8/simplejson-4.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c44ef6b02a4eb67ed17a72342341792149b3ff46f15426c26e970e49addf327", size = 178133, upload-time = "2026-04-24T19:22:54.574Z" },
{ url = "https://files.pythonhosted.org/packages/5d/40/ed806f24afef295c1032448f5ff6f6f2979392d5645ddb9f4fed7f38194d/simplejson-4.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82bfca2b85a34178c25829c703f0a9e9f113a5af7539285bd3efb583a0bf1ba3", size = 188155, upload-time = "2026-04-24T19:22:56.044Z" },
{ url = "https://files.pythonhosted.org/packages/38/94/8d6f515b827b0f7881a49c8c1ac6920b7ae9428939ef04238c973278b42a/simplejson-4.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e4b23f71dd781f8830f1663dc01a4944d3dbf87a1f93d78fba1cf64722d0ccf", size = 176225, upload-time = "2026-04-24T19:22:57.981Z" },
{ url = "https://files.pythonhosted.org/packages/c9/fd/6dffb4956563d48bbe46b91ff341adae34920e94008fd6b8d728072abfc7/simplejson-4.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:82fee635d7b73ad801030b05a75fbd34a098da0c2ecf600667a03636d09e1e42", size = 185535, upload-time = "2026-04-24T19:22:59.618Z" },
{ url = "https://files.pythonhosted.org/packages/de/d2/a509ee37763e79aec75d68f8521db1440306edeba3b8b4064ab4ee8bf1d9/simplejson-4.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:68e62eda21192c5ea9bb92d571ca46a4477fef48762f50d433de2b4253051551", size = 179302, upload-time = "2026-04-24T19:23:01.324Z" },
{ url = "https://files.pythonhosted.org/packages/d8/23/5b343bfd2a79d3b6818e4db3586c405a001a090d4c89d336e31273ce7177/simplejson-4.1.1-cp311-cp311-win32.whl", hash = "sha256:ffd3d82294b47f5ec64050021ace95fd62628a0c1cc8bbf4d06d2d1fb697e055", size = 88408, upload-time = "2026-04-24T19:23:02.808Z" },
{ url = "https://files.pythonhosted.org/packages/38/04/df9b37aedbd524dca20840d25ebe01d6ae486b89792aeff5d15b9c4114f7/simplejson-4.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:78a3fe0995be42bed62a26aa78e0e0b4d87c6545785346b9cc898f3389569a35", size = 90526, upload-time = "2026-04-24T19:23:04.408Z" },
{ url = "https://files.pythonhosted.org/packages/60/25/e90998fe8e480eb43b966c09e835379887d427567ebd496563d3b1e16b19/simplejson-4.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:19040a17154dc03d289bab68d73ce0a6a0be01de30c584bbdd93490bead14b22", size = 112414, upload-time = "2026-04-24T19:23:06.084Z" },
{ url = "https://files.pythonhosted.org/packages/9c/a0/abd4785f36c3400f1fbb21f517be39295a750a714f04b7ee175adf6ef580/simplejson-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a94ebaecdbaa80d9551a3ec6bf0c9302fc8b53ab6c1b2bfd498a1df4cb28158d", size = 91120, upload-time = "2026-04-24T19:23:07.877Z" },
{ url = "https://files.pythonhosted.org/packages/b8/78/fc060d2e3b13c6ec59288574b8efac64075e316b2afba4396a56b2422f78/simplejson-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67341c95c0a168ab4a6d1e807e50463f1c8da932c3286d81e201266c427061fa", size = 91055, upload-time = "2026-04-24T19:23:09.264Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b6/156a8de1e1b47694f0e7de6675866936608d45dc68388fd017d36f8693be/simplejson-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45ec18e337fec538b7e902d489505c450b2454653d1290f3f50385e6fd8aa607", size = 190297, upload-time = "2026-04-24T19:23:11.226Z" },
{ url = "https://files.pythonhosted.org/packages/86/1c/e4d0eab695be3eb21d0f46bce820752031f03e7113f9c80a9b3c73ee7157/simplejson-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:820c69a4710400e9b248d5670647d60be58824369282d3925e516b3ff1a7cd82", size = 187002, upload-time = "2026-04-24T19:23:12.982Z" },
{ url = "https://files.pythonhosted.org/packages/76/0e/7f5a59d29426b062d5928fb88b403c3f797129d53be7102f955dbe51aa44/simplejson-4.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e708d373a10e4378ef2d59f8361850c7150fd907ed49efe49bc5492160476d1", size = 195146, upload-time = "2026-04-24T19:23:14.517Z" },
{ url = "https://files.pythonhosted.org/packages/78/18/9943db224dd4d5fa3c090c3e56a94c37b254338c83995ec5680285111c40/simplejson-4.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:980fc33353f81fd12d8c49d44f8c2760d1dc8192285e627c5180d141035b228a", size = 183931, upload-time = "2026-04-24T19:23:16.742Z" },
{ url = "https://files.pythonhosted.org/packages/c2/08/9a690da9a766161c06c627d805362cf159f1abe480969372b2897649b955/simplejson-4.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:de2ed102fff88dacf543699f53ee3a533cc11539a39baa176b7e09dd783069d6", size = 192228, upload-time = "2026-04-24T19:23:18.33Z" },
{ url = "https://files.pythonhosted.org/packages/05/88/bd8aad36b451ffb0e0a3f721d695a88befa6d1ac7d1e02ae788ca7ff4029/simplejson-4.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785ff8edc0e28bf773a32543a6bbed46351453c997b3f6709c744e3c2f7eabb", size = 187808, upload-time = "2026-04-24T19:23:21.165Z" },
{ url = "https://files.pythonhosted.org/packages/04/ee/14f91db0d1f481533b651dafbf8cd0da088d9817f7af30c68f7f19f9c847/simplejson-4.1.1-cp312-cp312-win32.whl", hash = "sha256:2e0d5ead6d14610467ec356ec1f6b5d8a56aa216abaad8d41c8b873b16cf313f", size = 88512, upload-time = "2026-04-24T19:23:22.764Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c4/90de06b2d8737c68c05ff9274113f854dbf6a5f28b7a955212111672cb57/simplejson-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:63a5451f557d6be48a231bae932458655c620902b868170b2f1c8afed496f6b4", size = 90748, upload-time = "2026-04-24T19:23:24.494Z" },
{ url = "https://files.pythonhosted.org/packages/ce/6a/8b74c52ffd33dbbde00fe7251fee6a0acdc8cea33f7a43805aed258fb79b/simplejson-4.1.1-py3-none-any.whl", hash = "sha256:2ce92b3748f02423e26d2bfb636fb9d7a8f67c8f5854dcae69d350d123b2eee2", size = 69195, upload-time = "2026-04-24T19:24:57.962Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@@ -6203,16 +6449,16 @@ wheels = [
[[package]]
name = "workos"
version = "6.0.8"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "httpx" },
{ name = "pyjwt", extra = ["crypto"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/ca/0d/0a7f78912657f99412c788932ea1f3f4089916e77bdef7d2463842febe08/workos-6.0.8.tar.gz", hash = "sha256:43aa3f1992a0a4ca8933d9b6e5ada846dd3b1fe0ee10e64c876ee2000fc6090d", size = 178137, upload-time = "2026-04-24T18:48:03.203Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/f6/bb27fe77e70b5e2c5da72500ca0ece8b0e8318010fec92c31d68483314e3/workos-8.3.0.tar.gz", hash = "sha256:07b66c2fb287adb593e4d77a2e6cb05b48bd8ff0b2722f343d18eeb5e14f7472", size = 201587, upload-time = "2026-06-30T15:19:22.834Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/3f/3d96da80d650b2f97d58af626053354584f619dbb769051e118bd9cd1ca5/workos-6.0.8-py3-none-any.whl", hash = "sha256:a00dd4930333aded2babbba824f8032eea05c5ca8c44d04a3fa068cf6be6e21a", size = 524505, upload-time = "2026-04-24T18:48:01.389Z" },
{ url = "https://files.pythonhosted.org/packages/49/ed/7e6fe07c5bc0222fd92c1cf1f3c4c24293e4e5fc7bb5d7df90e4ee61c17f/workos-8.3.0-py3-none-any.whl", hash = "sha256:d0fa842b93bfc5fb33bf49e69cf8c379936cf54b87c6e2f50bcc6dd2e84f8fe4", size = 592275, upload-time = "2026-06-30T15:19:21.333Z" },
]
[[package]]
@@ -18,15 +18,12 @@ spec:
triggers:
- type: {{ .Values.worker.keda.triggerType }}
metadata:
userName: "postgres"
passwordFromEnv: POSTGRES_ADMIN_PASSWORD
host: {{ .Release.Name }}-postgresql
port: {{ .Values.postgresql.port | quote }}
dbName: {{ .Values.postgresql.auth.database | quote }}
sslmode: disable
# Query for KEDA to count the number of scans that are in executing, available, or scheduled states,
# where the scheduled time is within the last 2 hours and is before NOW(). Used for scaling workers.
query: >-
SELECT COUNT(*) FROM scans WHERE ((state='executing' OR state='available' OR state='scheduled') and scheduled_at < NOW() and scheduled_at > NOW() - INTERVAL '2 hours')
targetQueryValue: "1"
userName: {{ .Values.worker.keda.postgresql.userName | quote }}
passwordFromEnv: {{ .Values.worker.keda.postgresql.passwordFromEnv | quote }}
host: {{ .Values.worker.keda.postgresql.host | default (printf "%s-postgresql.%s.svc.cluster.local" .Release.Name .Release.Namespace) | quote }}
port: {{ .Values.worker.keda.postgresql.port | quote }}
dbName: {{ .Values.worker.keda.postgresql.database | default .Values.postgresql.auth.database | quote }}
sslmode: {{ .Values.worker.keda.postgresql.sslmode | quote }}
query: {{ .Values.worker.keda.query | quote }}
targetQueryValue: {{ .Values.worker.keda.targetQueryValue | quote }}
{{- end }}
+54 -3
View File
@@ -427,10 +427,61 @@ worker:
pollingInterval: 30
# -- The cooldown period in seconds for scaling
cooldownPeriod: 120
# -- The trigger type for scaling (cpu or memory)
# -- The KEDA scaler type. Only `postgresql` is supported by the default query below.
triggerType: "postgresql"
# -- The target utilization percentage for the worker pods
value: "50"
# PostgreSQL connection used by the scaler query. The KEDA operator opens this
# connection from its own namespace, so `host` must resolve from there. The
# defaults target the bundled postgresql subchart; set them explicitly when
# using an external database (postgresql.enabled: false).
postgresql:
# -- Scaler database host. Defaults to the bundled "<release>-postgresql.<namespace>.svc.cluster.local" service.
host: ""
# -- Scaler database port.
port: "5432"
# -- Scaler database name. Defaults to `postgresql.auth.database`.
database: ""
# -- User the scaler authenticates as.
userName: "postgres"
# -- Name of an env var on the worker container holding the password.
passwordFromEnv: "POSTGRES_ADMIN_PASSWORD"
# -- sslmode for the scaler connection.
sslmode: "disable"
# -- The scaler divides the query result by this value to get the desired replica count.
targetQueryValue: "1"
# -- Query the scaler runs to measure pending work. It replaces the previous
# 2-hour scheduled-only window, which missed manual scans, older backlogs and
# in-progress scans. Override to tune scaling for your workload.
#
# The default sums three signals:
# 1. Scans executing or available, bounded to rows updated in the last 24h so
# orphaned rows do not pin the worker up, plus scheduled scans that are due
# (no lower bound, so an overdue backlog still scales up).
# 2. Scan tasks published in the last 48h that no worker has finished. A PENDING
# TaskResult is written at publish time (before_task_publish in api/signals.py),
# so Beat's daily publishes are visible even with zero workers. Signal 1 alone
# deadlocks with minReplicas 0: every scan row after the first is created by
# the worker, so once the initial row ages out of the 24h bound there is
# nothing to count and nothing to create more.
# 3. Non-scan tasks pending in the last hour. Provider connection checks,
# deletions, reports and backfills never touch the scans table, so without
# this they are never picked up while the worker is scaled to zero.
# This includes reconcile-orphan-tasks, a Beat watchdog that runs every two
# minutes, so with minReplicas 0 the worker is woken about that often. Add
# it to the excluded task names below, or raise cooldownPeriod, if you would
# rather trade watchdog latency for longer idle periods.
query: >-
SELECT
(SELECT COUNT(*) FROM scans
WHERE (state IN ('executing', 'available') AND updated_at > NOW() - INTERVAL '24 hours')
OR (state = 'scheduled' AND scheduled_at < NOW()))
+ (SELECT COUNT(*) FROM django_celery_results_taskresult
WHERE task_name IN ('scan-perform', 'scan-perform-scheduled')
AND status IN ('PENDING', 'RECEIVED', 'STARTED')
AND date_created > NOW() - INTERVAL '48 hours')
+ (SELECT COUNT(*) FROM django_celery_results_taskresult
WHERE task_name NOT IN ('scan-perform', 'scan-perform-scheduled')
AND status IN ('PENDING', 'RECEIVED', 'STARTED')
AND date_created > NOW() - INTERVAL '1 hour')
worker_beat:
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
+2 -2
View File
@@ -64,7 +64,7 @@ services:
condition: service_healthy
postgres:
image: postgres:16.3-alpine3.20@sha256:36ed71227ae36305d26382657c0b96cbaf298427b3f1eaeb10d77a6dea3eec41
image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
@@ -88,7 +88,7 @@ services:
retries: 5
valkey:
image: valkey/valkey:7-alpine3.19@sha256:4054fe7fc607b9326ac7c4691ed26e9670d2ff17a9fb28c2577adecf928acbcc
image: valkey/valkey:8-alpine@sha256:a038175878d66b9d274fbf8be73c0305e93798b83917647f167e18cef3c71eec
hostname: "valkey"
volumes:
- ./_data/valkey:/data
+2 -2
View File
@@ -60,7 +60,7 @@ services:
start_period: 60s
postgres:
image: postgres:16.3-alpine3.20@sha256:36ed71227ae36305d26382657c0b96cbaf298427b3f1eaeb10d77a6dea3eec41
image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
@@ -80,7 +80,7 @@ services:
retries: 5
valkey:
image: valkey/valkey:7-alpine3.19@sha256:4054fe7fc607b9326ac7c4691ed26e9670d2ff17a9fb28c2577adecf928acbcc
image: valkey/valkey:8-alpine@sha256:a038175878d66b9d274fbf8be73c0305e93798b83917647f167e18cef3c71eec
hostname: "valkey"
volumes:
- ./_data/valkey:/data
+27 -15
View File
@@ -41,7 +41,7 @@ Every GitHub Actions workflow uses runner hardening, pinned action versions, and
### Workflow Security Audit With Zizmor
- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs via [`ci-zizmor.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ci-zizmor.yml).
- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs on every pull request and push.
- Triggers on every push, every pull request that touches `.github/`, and on a daily schedule.
- Results upload to the GitHub Security tab via Static Analysis Results Interchange Format (SARIF).
- Key [audit rules](https://docs.zizmor.sh/audits/) the build gates on:
@@ -65,19 +65,19 @@ Multiple SAST tools run on every push and pull request to catch vulnerabilities
### Cross-Language
- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan, via [`sdk-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-codeql.yml), [`api-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-codeql.yml), and [`ui-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-codeql.yml). Results upload to the GitHub Security tab via SARIF.
- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan. Results upload to the GitHub Security tab via SARIF.
### Python (SDK + API)
- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every PR/push in [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml) and [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml).
- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml) and [`api-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-code-quality.yml).
- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every PR/push in `sdk-security.yml` and `api-security.yml`.
- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml).
- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every pull request and push.
- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every pull request and push.
- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every pull request and push.
- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every pull request and push.
### JavaScript/TypeScript (UI)
- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every PR/push via `pnpm run healthcheck` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml).
- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every PR/push via `pnpm run healthcheck` in `ui-tests.yml`.
- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every pull request and push via `pnpm run healthcheck`.
- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every pull request and push via `pnpm run healthcheck`.
- **Knip:** dead-code and unused-export detection for the UI. The UI analogue to Vulture.
<Note>
@@ -94,12 +94,12 @@ Dependencies are scanned against public vulnerability databases on every pull re
### Cross-Language
- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs via [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml), [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml), and [`ui-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-security.yml).
- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs on every pull request and push.
- The action installs the `osv-scanner` binary and verifies its SHA-256 checksum against the upstream-signed `SHA256SUMS` manifest before running. Any mismatch aborts the scan.
- Gates the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings.
- Posts and updates a per-lockfile report as a pull request comment.
- Per-vulnerability ignores live in [`osv-scanner.toml`](https://github.com/prowler-cloud/prowler/blob/master/osv-scanner.toml) at the repo root, each with a reason and an expiry date.
- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR.
- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs on every pull request and push that touches an image or its dependencies. Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR.
- **Dependabot:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml) for monthly updates of the SDK Python dependencies, GitHub Actions, Docker base images, and pre-commit hooks. Dependabot opens pull requests for known security advisories, so critical patches reach the team without delay. A 7-day default cooldown reduces exposure to compromised package releases.
- **Renovate:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/renovate.json) dependency update automation is transitioning from Dependabot to **Renovate** to gain finer control over update cadence, grouping, and per-component scope. Both tools currently run in parallel during the migration.
@@ -126,7 +126,7 @@ Dependabot is paused for the API and UI; Renovate now handles those components.
### JavaScript/TypeScript (UI)
- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml). Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier.
- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit`. Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier.
## Supply-Chain Pinning
@@ -150,7 +150,7 @@ The controls applied across all three:
- **uv itself pinned** in the [`setup-python-uv`](https://github.com/prowler-cloud/prowler/tree/master/.github/actions/setup-python-uv) composite action.
<Note>
The MCP Server has a small direct-dependency surface and does not yet declare a separate constraint set. Its lock file is the source of truth.
The MCP Server declares a small constraint set of its own, covering transitive pins that `fastmcp` does not raise on its own. Its lock file remains the source of truth for everything else.
</Note>
### JavaScript/TypeScript (pnpm)
@@ -181,8 +181,8 @@ Container images get scanned twice: once in CI before they push to a registry, a
### Pre-Publish (CI)
- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds can fail on critical findings when configured to.
- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and in the same `*-container-checks.yml` workflows linked above.
- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs on every pull request and push that touches an image or its dependencies. Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds fail on any critical finding that is not explicitly accepted. Accepted findings live in [`.trivyignore`](https://github.com/prowler-cloud/prowler/blob/master/.trivyignore), each carrying a reason and an expiry date, the same policy `osv-scanner.toml` follows. A local `trivy image` run applies the same suppressions whenever it is launched from a directory containing `.trivyignore`, which Trivy loads automatically. To see the unsuppressed set, run it from elsewhere or point `--ignorefile` at an empty file.
- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and alongside the image scans above.
### Post-Publish (Registries)
@@ -190,9 +190,21 @@ Container images get scanned twice: once in CI before they push to a registry, a
- **Docker Hub:** Docker Hub continuously scans the same images mirrored from ECR.
- The security team reviews findings from both registries for triage and remediation.
### Known Findings
A small number of findings remain in the published images and cannot be resolved by Prowler: the upstream project has released no fix, the package cannot be removed without breaking the image, or the finding comes from a vendored SBOM rather than from a package that is actually installed. Alternative base distributions have been evaluated and none currently satisfies both the vulnerability profile and the runtime requirements of every supported provider.
Each suppression is recorded in [`.trivyignore`](https://github.com/prowler-cloud/prowler/blob/master/.trivyignore) with the reason it cannot be fixed, why it is not exploitable in Prowler's runtime, and an expiry date that forces re-review. Nothing is suppressed without that rationale, and a build fails on any critical finding that is not listed there.
To see the current set for any image, scan it directly:
```bash
trivy image prowlercloud/prowler:latest
```
## Secrets Detection
- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request via [`find-secrets.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/find-secrets.yml). Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings.
- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request. Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings.
- A pre-commit hook runs the same check locally and blocks secrets before they leave the developer machine.
## Security Monitoring
+8
View File
@@ -4,6 +4,14 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
<!-- changelog: release notes start -->
## [0.9.1] (Prowler v5.37.1)
### 🔐 Security
- Bumped `fastmcp` and pinned `cryptography`, `joserfc`, `mcp` and `python-multipart`, clearing all 7 high-severity CVEs from the MCP image [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
---
## [0.9.0] (Prowler v5.37.0)
### 🚀 Added
@@ -0,0 +1 @@
Upgrade cryptography to 50.0.0, closing CVE-2026-69247 and CVE-2026-69249
+9 -1
View File
@@ -12,7 +12,7 @@ dev = [
[project]
dependencies = [
"fastmcp==3.4.4",
"fastmcp==3.4.5",
"httpx==0.28.1"
]
description = "MCP server for Prowler ecosystem"
@@ -47,3 +47,11 @@ extend-select = [
[tool.uv]
package = true
# Transitive pins fastmcp does not raise on its own; each carries a known HIGH.
constraint-dependencies = [
"cryptography==50.0.0",
"joserfc==1.6.8",
"mcp==1.28.1",
"python-multipart==0.0.30"
]
+71 -60
View File
@@ -1,6 +1,20 @@
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform != 'win32'",
"python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14' and sys_platform != 'win32'",
]
[manifest]
constraints = [
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "joserfc", specifier = "==1.6.8" },
{ name = "mcp", specifier = "==1.28.1" },
{ name = "python-multipart", specifier = "==0.0.30" },
]
[[package]]
name = "aiofile"
@@ -201,55 +215,52 @@ wheels = [
[[package]]
name = "cryptography"
version = "48.0.0"
version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
]
[[package]]
@@ -321,19 +332,19 @@ wheels = [
[[package]]
name = "fastmcp"
version = "3.4.4"
version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fastmcp-slim", extra = ["client", "server"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/f7/5188565d1b93ad611cbd80bf473e7ad669d1f3b689c4bedcd304e1ec3472/fastmcp-3.4.4.tar.gz", hash = "sha256:378202e26ec15b23819d9a1c0d1b0ebda096bc712720532010a0b82a45c2b1df", size = 28796458, upload-time = "2026-07-09T00:32:41.352Z" }
sdist = { url = "https://files.pythonhosted.org/packages/23/14/c1ffb91b7d1fece86c81e1f9df5474f30fd97e4cdaa398814bbbeee88568/fastmcp-3.4.5.tar.gz", hash = "sha256:a95f2bc876bef42e8b50f7872f24f3f2fe3b1d37408c734e8b9d9e03014b72d3", size = 28800521, upload-time = "2026-07-27T19:20:01.231Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/67/3cef84ba38a23dca1e1e776bfda8a35ab3c7a6c94a8ca81d0715de6dd3c5/fastmcp-3.4.4-py3-none-any.whl", hash = "sha256:f86f208713212260068cf55c32936839eee856fefc7808e18a032f31eb0f718e", size = 8019, upload-time = "2026-07-09T00:32:39.411Z" },
{ url = "https://files.pythonhosted.org/packages/c6/4f/73450a436c963c0382d15a882fc5d08f15aadc329194df1b54495a7c8383/fastmcp-3.4.5-py3-none-any.whl", hash = "sha256:5d3d438eb2917e63e6faf53e8cb8fe26d887ec3232f848093a4eecad7fa34861", size = 8017, upload-time = "2026-07-27T19:19:57.942Z" },
]
[[package]]
name = "fastmcp-slim"
version = "3.4.4"
version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "platformdirs" },
@@ -343,9 +354,9 @@ dependencies = [
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/79/f35661c6a1d76dfbe17a079f912d96fffcfdd40fad5a9144bb9e7dfb1fdf/fastmcp_slim-3.4.4.tar.gz", hash = "sha256:dcaa3e0be2127d7eacdce592c2ef0039204923dc0ec396454615cb4a3275b078", size = 590203, upload-time = "2026-07-09T00:32:20.531Z" }
sdist = { url = "https://files.pythonhosted.org/packages/81/1d/f3e271fbcd01ce01a4cf623b336d8e1305c192aa5d5e8e0223b7167462e9/fastmcp_slim-3.4.5.tar.gz", hash = "sha256:5badc3bceee61f61297eeb9494f499325f3ce1cafabf4611b31f6c3e9d7dff59", size = 591622, upload-time = "2026-07-27T19:15:19.455Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/91/321e0b2e9ed70d0628b17ddaec76fc7b09f3e1d5d290f70bf101a2890142/fastmcp_slim-3.4.4-py3-none-any.whl", hash = "sha256:9d3a6327b9ee835188eb7323fc3b5d4cd061631b48da8ece56794bb538972505", size = 765158, upload-time = "2026-07-09T00:32:19.11Z" },
{ url = "https://files.pythonhosted.org/packages/43/3b/16d8aa8224094519f30b078138e725b8a731bf0a13f1f850e58b5f9b3cc4/fastmcp_slim-3.4.5-py3-none-any.whl", hash = "sha256:bc31217827c4999812543c83ee95ed9a47f3ed1e3fd0bd4f64371e375b748eca", size = 766478, upload-time = "2026-07-27T19:15:18.015Z" },
]
[package.optional-dependencies]
@@ -511,14 +522,14 @@ wheels = [
[[package]]
name = "joserfc"
version = "1.6.5"
version = "1.6.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5d/ac/d4fd5b30f82900eac60d765f179f0ba005825ac462cc8ced6e13ec685ab3/joserfc-1.6.8.tar.gz", hash = "sha256:878620c553a6ebdd76ccdc356782fee3f735f21a356d079a546b42a4670ace5f", size = 232930, upload-time = "2026-05-27T03:22:37.819Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" },
{ url = "https://files.pythonhosted.org/packages/98/8c/5cdce2cf3ce8155849baf9a5e2ce77e89dc87ec3bdb38259e5d85fbc45bd/joserfc-1.6.8-py3-none-any.whl", hash = "sha256:22fb31a69094a5e6f44632002a9df2c30c941fc6c8ce1b037e92c03de954cf9f", size = 70927, upload-time = "2026-05-27T03:22:35.796Z" },
]
[[package]]
@@ -602,7 +613,7 @@ wheels = [
[[package]]
name = "mcp"
version = "1.27.1"
version = "1.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -620,9 +631,9 @@ dependencies = [
{ name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" },
{ url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
]
[[package]]
@@ -723,7 +734,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "fastmcp", specifier = "==3.4.4" },
{ name = "fastmcp", specifier = "==3.4.5" },
{ name = "httpx", specifier = "==0.28.1" },
]
@@ -937,11 +948,11 @@ wheels = [
[[package]]
name = "python-multipart"
version = "0.0.28"
version = "0.0.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" },
{ url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" },
]
[[package]]
+23
View File
@@ -4,6 +4,29 @@ All notable changes to the **Prowler SDK** are documented in this file.
<!-- changelog: release notes start -->
## [5.37.1] (Prowler v5.37.1)
### 🔄 Changed
- Huawei Cloud exception codes moved from `19000`-`19007` to `20000`-`20007`, resolving a collision with E2E Networks which reserves `19000`-`19999` [(#12306)](https://github.com/prowler-cloud/prowler/pull/12306)
### 🐞 Fixed
- Checks registered through the `prowler.checks.<provider>` entry-point group can now run against built-in providers. The built-in probe in `_resolve_check_module` used a bare `find_spec`, which imports the parent package to search it and so raised `ModuleNotFoundError` for a plug-in check instead of returning `None`, aborting the lookup before the entry points were consulted. Such a check was discovered, listed and selected for execution, then silently produced no findings. [(#12312)](https://github.com/prowler-cloud/prowler/pull/12312)
- Entra Conditional Access guest-user checks no longer report false FAILs: microsoft-kiota packages bumped to 1.9.10 so `guestOrExternalUserTypes` (a flags enum Graph serializes as a comma-separated string) deserializes correctly instead of returning an empty list [(#12315)](https://github.com/prowler-cloud/prowler/pull/12315)
### 🔐 Security
- Bumped the Compose `postgres` and `valkey` images, clearing 10 critical CVEs [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
- Bumped PowerShell, Trivy, uv and `joserfc` in the container images, clearing 14 high-severity CVEs from the SDK and API images [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
- Bumped `httplib2` to 0.32.0 and `pyasn1` to 0.6.4 to resolve known CVEs [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
- The SDK container image now builds on Debian 13 (trixie), clearing the unfixable `libsqlite3-0` and `zlib1g` criticals [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
- Bumped `cryptography` to 48.0.1 to resolve GHSA-537c-gmf6-5ccf, along with the `oci`, `alibabacloud-tea-openapi`, `darabonba-core` and `py-ocsf-models` bumps it requires [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
- Removed `pip` from the SDK container image, clearing two high-severity CVEs in the vendored copies of `setuptools` and `msgpack` [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
- Removed `wget`, `gnupg` and `apt-transport-https` from the SDK runtime image [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
---
## [5.37.0] (Prowler v5.37.0)
### 🚀 Added
@@ -0,0 +1 @@
Alibaba Cloud STS credential validation retries transient connection failures and reports exhausted attempts as connection errors instead of invalid credentials
@@ -0,0 +1 @@
Upgrade cryptography to 50.0.0, closing CVE-2026-69247 and CVE-2026-69249
@@ -0,0 +1 @@
The `securityhub_delegated_admin_enabled_all_regions`, `guardduty_delegated_admin_enabled_all_regions` and `config_delegated_admin_and_org_aggregator_all_regions` checks now report MANUAL instead of FAIL when the delegated administrator status cannot be read and no independent misconfiguration is detected, which happens on member accounts that are not registered as delegated administrators because the API is restricted to the organization management account and to delegated administrator accounts
@@ -0,0 +1 @@
`guardduty_delegated_admin_enabled_all_regions` no longer reports `no delegated administrator configured` when the lookup was denied or failed, which asserted absence where there was only lack of visibility
@@ -0,0 +1 @@
Secret checks no longer report credential-free JDBC connection strings as embedded credentials
@@ -0,0 +1 @@
OCI Identity service no longer drops the whole dynamic groups, groups, policies or users listing when the OCI API returns null optional fields such as `matching_rule`
@@ -0,0 +1 @@
A failed `ListOrganizationAdminAccounts` lookup in one region no longer marks the Security Hub delegated administrator status as undetermined in every other region
@@ -0,0 +1 @@
`securityhub_delegated_admin_enabled_all_regions` no longer reports FAIL with `delegated administrator status could not be determined` on accounts that do have a Security Hub delegated administrator; `ListOrganizationAdminAccounts` responses are now parsed with the `AccountId` and `Status` fields the API actually returns
+1 -1
View File
@@ -49,7 +49,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.37.0"
prowler_version = "5.37.2"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
+20 -14
View File
@@ -21,7 +21,11 @@ from prowler.lib.check.utils import recover_checks_from_provider
from prowler.lib.logger import logger
from prowler.lib.outputs.outputs import report
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
from prowler.providers.common.builtin import is_builtin_provider
from prowler.providers.common.builtin import (
builtin_check_module,
is_builtin_check,
is_builtin_provider,
)
from prowler.providers.common.models import Audit_Metadata
@@ -401,21 +405,23 @@ def _resolve_check_module(
when a plug-in tries to override, so the user knows their plug-in
duplicate is being ignored and can rename it.
Gates the built-in branch on `is_builtin_provider(provider_type)`
calling `find_spec` on `prowler.providers.{provider_type}.services...`
directly would propagate `ModuleNotFoundError` for external providers
(their parent package `prowler.providers.{provider_type}` does not
exist) instead of returning None. The leaf helper encapsulates the
safe lookup, so external providers go straight to entry points. For
built-ins we still use `find_spec` to distinguish "check doesn't
exist" from "check exists but failed to import" (broken transitive
dep, etc.).
Both probes are gated on leaf helpers rather than a raw `find_spec`,
because `find_spec` imports the parent package in order to search it and
so propagates `ModuleNotFoundError` instead of returning None whenever
that parent is absent. That happens on both axes: for an external
provider (no `prowler.providers.{provider_type}` package) and, on a
built-in provider, for an external check (no
`prowler.providers.{provider_type}.services.{service}.{check_name}`
package). Either one, probed naively, aborts the lookup before the entry
points are ever consulted. `is_builtin_check` still distinguishes "check
doesn't exist" from "check exists but failed to import" (broken
transitive dep, etc.), which a blanket except would flatten.
"""
# Built-in first — built-in wins on CheckID collision
if is_builtin_provider(provider_type):
builtin_path = f"prowler.providers.{provider_type}.services.{service}.{check_name}.{check_name}"
if importlib.util.find_spec(builtin_path) is not None:
return import_check(builtin_path)
if is_builtin_provider(provider_type) and is_builtin_check(
provider_type, service, check_name
):
return import_check(builtin_check_module(provider_type, service, check_name))
# Entry point lookup — only consulted when the built-in truly doesn't exist
for ep in importlib.metadata.entry_points(group=f"prowler.checks.{provider_type}"):
@@ -0,0 +1,97 @@
# Override of Kingfisher's built-in `kingfisher.jdbc.1`. Loading this file with
# `--rules-path` replaces the built-in rule of the same id (see
# `_build_kingfisher_command` in prowler/lib/utils/utils.py).
#
# The built-in pattern matches a bare `jdbc:<scheme>:` prefix plus any 10
# non-space characters, so every JDBC connection string is reported as an
# embedded credential even when it carries none. The defect is upstream
# (https://github.com/mongodb/kingfisher), still present in 1.110.0.
#
# Because this replaces the built-in rule rather than extending it, every field
# below other than `pattern` and `examples` is a verbatim copy of the built-in
# rule: dropping one would silently disable it. `validation` in particular is
# what makes `--scan-secrets-validate` confirm a JDBC credential is live, and
# `pattern_requirements` is what discards placeholder values.
#
# Drop this file when a `kingfisher-bin` bump makes the credential-free cases in
# `Test_detect_secrets_scan_batch_jdbc` pass without it.
rules:
- name: JDBC connection string with embedded credentials
id: kingfisher.jdbc.1
# Only this and `examples` diverge from the built-in rule. The `{1,32}`
# scheme quantifier (upstream uses `{2,32}`) also lets two-character schemes
# such as `jdbc:h2:` match.
#
# `#` is escaped even inside character classes: under `(?x)` a bare one
# opens a comment there too, and Kingfisher then fails to compile the rule
# and aborts the whole scan.
pattern: |
(?xi)
(
(?:
# Credential forms that any JDBC subprotocol can carry.
jdbc:
[a-z][a-z0-9+.-]{1,32}
(?:[:][a-z0-9+.-]{1,32})*
:
(?:
# URL userinfo, anchored to the `//` that opens the authority.
# Both halves also exclude `?&;#` so neither can reach into the
# query string or the property list looking for an `@`.
// [^\s"'<>/@:?&;\#]{1,64} : [^\s"'<>/@?&;\#]{1,64} @
# Password as a query parameter or a `;`-delimited property. Only
# this one keeps a leading `.*?`, because its match starts at the
# delimiter before the keyword, anywhere in the string.
| [^\s"'<>,(){}\[\]]{0,384}?
[?&;] [ \t]* (?:password|passwd|pwd|secret) [ \t]* = [ \t]* [^\s"'<>&;]{1,128}
)
# Oracle TNS userinfo, `jdbc:oracle:<drivertype>:user/password@db`.
# Spelled out as its own top-level alternative rather than as a third
# branch above, because `user/password@` is a credential only after an
# Oracle prefix: every other subprotocol reads `a/b@c` as part of a
# path or a host, so sharing the branch reported credential-free
# strings such as `jdbc:derby:team/ops@corp.internal`.
| jdbc:oracle:
[a-z0-9+.-]{1,32}
:
[^\s"'<>/@:?&;\#]{1,64} / [^\s"'<>/@?&;\#]{1,64} @
# MySQL Connector/J host-list credentials. Keep both forms anchored
# to the MySQL prefix so this syntax cannot affect other drivers.
| jdbc:mysql://
(?:
\(
[ \t]* host [ \t]* = [ \t]* [^,()\s"'<>/]{1,128}
[ \t]* , [ \t]* user [ \t]* = [ \t]* [^,()\s"'<>/]{1,64}
[ \t]* , [ \t]* password [ \t]* = [ \t]* [^,()\s"'<>/]{1,128}
[ \t]* \)
| address [ \t]* =
[ \t]* \( [ \t]* host [ \t]* = [ \t]* [^()\s"'<>/]{1,128} [ \t]* \)
[ \t]* \( [ \t]* user [ \t]* = [ \t]* [^()\s"'<>/]{1,64} [ \t]* \)
[ \t]* \( [ \t]* password [ \t]* = [ \t]* [^()\s"'<>/]{1,128} [ \t]* \)
)
)
[^\s"'<>,(){}\[\]]{0,192}
)
pattern_requirements:
min_special_chars: 2
special_chars: ";=/?@&"
ignore_if_contains:
- "****"
- "xxxx"
- "example"
min_entropy: 3.3
confidence: medium
validation:
type: Jdbc
tls_mode: lax
# Enforced at load time: Kingfisher rejects the rule if one does not match.
examples:
- "jdbc:mysql://admin:s3cr3t@prod.internal:3306/inventory" # trufflehog:ignore
- "jdbc:postgresql://db.example.com:5432/app?user=admin&password=s3cr3t" # trufflehog:ignore
- "jdbc:sqlserver://sql.example.org:1433;databaseName=inventory;user=sa;password=s3cr3t!" # trufflehog:ignore
- "jdbc:oracle:thin:scott/tiger@ora.example.net:1521:ORCLPDB1" # trufflehog:ignore
- "jdbc:h2:file:./data/store;CIPHER=AES;PASSWORD=filepwd" # trufflehog:ignore
references:
- https://docs.oracle.com/javase/8/docs/api/java/sql/DriverManager.html
- https://jdbc.postgresql.org/documentation/use/
- https://github.com/pgjdbc/pgjdbc/blob/3a699d57d957ca0c2b86e619d001a8763a130027/docs/content/documentation/use.md
+8
View File
@@ -47,6 +47,11 @@ default_secrets_batch_chunk_size = 500
# cannot block the audit indefinitely.
default_secrets_scan_timeout = 300
# Directory of Prowler-maintained Kingfisher rules, loaded with ``--rules-path``
# on every scan. A rule here that reuses a built-in id replaces the built-in one
# (see kingfisher_rules/*.yaml for why each override exists).
secrets_rules_path = os.path.join(os.path.dirname(__file__), "kingfisher_rules")
class SecretsScanError(Exception):
"""The secret scanner could not produce a trustworthy result.
@@ -86,6 +91,9 @@ def _build_kingfisher_command(
"--no-update-check",
"--confidence",
confidence,
# Overrides for built-in rules that produce false positives.
"--rules-path",
secrets_rules_path,
]
if validate:
# Live-validate discovered secrets against provider APIs. Use
@@ -1,11 +1,17 @@
import os
import pathlib
import socket
from alibabacloud_credentials.client import Client as CredClient
from alibabacloud_credentials.models import Config as CredConfig
from alibabacloud_sts20150401.client import Client as StsClient
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_openapi.exceptions import ClientException
from colorama import Fore, Style
from darabonba.exceptions import RetryError
from darabonba.policy.retry import RetryCondition, RetryOptions
from requests.exceptions import ConnectionError as RequestsConnectionError
from requests.exceptions import Timeout as RequestsTimeout
from prowler.config.config import (
default_config_file_path,
@@ -17,9 +23,12 @@ from prowler.lib.utils.utils import print_boxes
from prowler.providers.alibabacloud.config import (
ALIBABACLOUD_DEFAULT_REGION,
ALIBABACLOUD_REGIONS,
ALIBABACLOUD_STS_MAX_ATTEMPTS,
ALIBABACLOUD_STS_RETRY_DELAY_MS,
ROLE_SESSION_NAME,
)
from prowler.providers.alibabacloud.exceptions.exceptions import (
AlibabaCloudConnectionError,
AlibabaCloudInvalidCredentialsError,
AlibabaCloudNoCredentialsError,
AlibabaCloudSetUpSessionError,
@@ -34,6 +43,61 @@ from prowler.providers.common.models import Audit_Metadata, Connection
from prowler.providers.common.provider import Provider
def _exception_chain(error: Exception):
"""Yield structured exceptions wrapped by SDK and Python exception chains."""
pending = [error]
seen = set()
while pending:
current = pending.pop()
if id(current) in seen:
continue
seen.add(id(current))
yield current
for attribute in ("inner_exception", "__cause__", "__context__"):
nested = getattr(current, attribute, None)
if isinstance(nested, BaseException):
pending.append(nested)
pending.extend(arg for arg in current.args if isinstance(arg, BaseException))
def _is_connection_error(error: Exception) -> bool:
"""Return whether an SDK exception chain contains a transport failure."""
connection_errors = (
ConnectionError,
TimeoutError,
socket.gaierror,
RetryError,
RequestsConnectionError,
RequestsTimeout,
)
return any(
isinstance(exception, connection_errors)
for exception in _exception_chain(error)
)
def _is_authentication_error(error: Exception) -> bool:
"""Return whether an SDK exception chain contains an authentication failure."""
authentication_code_prefixes = (
"InvalidAccessKeyId",
"InvalidSecurityToken",
"MissingSecurityToken",
"SecurityTokenExpired",
"SignatureDoesNotMatch",
)
for exception in _exception_chain(error):
if not isinstance(exception, ClientException):
continue
code = exception.code or ""
if exception.status_code == 401 or code == "InvalidCredentials":
return True
if code.startswith(authentication_code_prefixes):
return True
return False
class AlibabacloudProvider(Provider):
"""
AlibabacloudProvider class is the main class for the Alibaba Cloud provider.
@@ -435,6 +499,7 @@ class AlibabacloudProvider(Provider):
AlibabaCloudCallerIdentity: An object containing the caller identity information.
Raises:
AlibabaCloudConnectionError: If STS cannot be reached after retries.
AlibabaCloudInvalidCredentialsError: If credentials are invalid.
"""
try:
@@ -445,6 +510,18 @@ class AlibabacloudProvider(Provider):
sts_config = open_api_models.Config(
access_key_id=cred.access_key_id,
access_key_secret=cred.access_key_secret,
retry_options=RetryOptions(
retryCondition=[
RetryCondition(
maxAttempts=ALIBABACLOUD_STS_MAX_ATTEMPTS,
exception=["RetryError"],
backoff={
"policy": "Fixed",
"period": ALIBABACLOUD_STS_RETRY_DELAY_MS,
},
)
]
),
)
if cred.security_token:
sts_config.security_token = cred.security_token
@@ -477,10 +554,17 @@ class AlibabacloudProvider(Provider):
except Exception as sts_error:
logger.error(f"Could not get caller identity from STS: {sts_error}. ")
raise AlibabaCloudInvalidCredentialsError(
file=pathlib.Path(__file__).name,
original_exception=sts_error,
)
if _is_authentication_error(sts_error):
raise AlibabaCloudInvalidCredentialsError(
file=pathlib.Path(__file__).name,
original_exception=sts_error,
) from sts_error
if _is_connection_error(sts_error):
raise AlibabaCloudConnectionError(
file=pathlib.Path(__file__).name,
original_exception=sts_error,
) from sts_error
raise
@staticmethod
def get_profile_region() -> str:
@@ -742,6 +826,7 @@ class AlibabacloudProvider(Provider):
Raises:
AlibabaCloudSetUpSessionError: If there is an error setting up the session.
AlibabaCloudConnectionError: If STS cannot be reached after retries.
AlibabaCloudInvalidCredentialsError: If there is an authentication error.
Exception: If there is an unexpected error.
@@ -809,6 +894,14 @@ class AlibabacloudProvider(Provider):
raise auth_error
return Connection(error=auth_error)
except AlibabaCloudConnectionError as connection_error:
logger.error(
f"{connection_error.__class__.__name__}[{connection_error.__traceback__.tb_lineno}]: {connection_error}"
)
if raise_on_exception:
raise connection_error
return Connection(error=connection_error)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
+4
View File
@@ -6,6 +6,10 @@ ROLE_SESSION_NAME = "ProwlerAssessmentSession"
# Alibaba Cloud SDK Configuration
ALIBABACLOUD_SDK_READ_TIMEOUT = 60 # seconds
ALIBABACLOUD_SDK_CONNECT_TIMEOUT = 10 # seconds
ALIBABACLOUD_STS_MAX_ATTEMPTS = 3
# Avoid immediate retry bursts while bounding added retry delay to two seconds:
# three total attempts introduce at most two fixed one-second waits.
ALIBABACLOUD_STS_RETRY_DELAY_MS = 1000
# Alibaba Cloud Regions - Only publicly accessible regions
# Note: Some regions may require special approval or are not globally available
@@ -38,6 +38,10 @@ class AlibabaCloudBaseException(ProwlerException):
"message": "Alibaba Cloud HTTP/API error",
"remediation": "Check the Alibaba Cloud API request and response, and ensure the service is accessible.",
},
(10008, "AlibabaCloudConnectionError"): {
"message": "Could not connect to Alibaba Cloud",
"remediation": "Check network connectivity and ensure the Alibaba Cloud service endpoint is accessible.",
},
}
def __init__(self, code, file=None, original_exception=None, message=None):
@@ -114,3 +118,12 @@ class AlibabaCloudHTTPError(AlibabaCloudBaseException):
super().__init__(
10007, file=file, original_exception=original_exception, message=message
)
class AlibabaCloudConnectionError(AlibabaCloudBaseException):
"""Raised when Alibaba Cloud cannot be reached after retry attempts."""
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
10008, file=file, original_exception=original_exception, message=message
)
@@ -71,12 +71,7 @@ class config_delegated_admin_and_org_aggregator_all_regions(Check):
covers_all = aggregator.all_aws_regions
issues = []
if delegated_admin_unknown:
issues.append(
"delegated administrator status for config.amazonaws.com "
"could not be determined"
)
elif not has_delegated_admin:
if not delegated_admin_unknown and not has_delegated_admin:
issues.append(
"no delegated administrator registered for config.amazonaws.com"
)
@@ -95,6 +90,25 @@ class config_delegated_admin_and_org_aggregator_all_regions(Check):
f"AWS Config aggregator {aggregator.name} in region "
f"{region} has issues: {', '.join(issues)}."
)
if delegated_admin_unknown:
report.status_extended = (
f"{report.status_extended[:-1]}; the delegated "
f"administrator status for config.amazonaws.com could "
f"not be determined."
)
elif delegated_admin_unknown:
# Not being able to read the delegated administrator is a lack
# of visibility, not a misconfiguration: the Organizations API
# is only available to the management or delegated
# administrator account.
report.status = "MANUAL"
report.status_extended = (
f"AWS Config aggregator {aggregator.name} in region {region} "
f"is an organization aggregator covering all AWS regions, but "
f"the delegated administrator status for config.amazonaws.com "
f"could not be determined; run this check from the "
f"organization management or delegated administrator account."
)
else:
report.status = "PASS"
report.status_extended = (
@@ -32,6 +32,15 @@ class guardduty_delegated_admin_enabled_all_regions(Check):
# Check if this region has a delegated admin
has_delegated_admin = detector.region in regions_with_admin
# The lookup is tracked per region so that a failure in one region does
# not mask the result of the others. A failure is only relevant when no
# delegated admin was found: if one was, the status is known.
admin_lookup_failed = (
not has_delegated_admin
and detector.region
in guardduty_client.organization_admin_lookup_failed_regions
)
# Check if detector is enabled
detector_enabled = detector.enabled_in_account and detector.status
@@ -43,7 +52,7 @@ class guardduty_delegated_admin_enabled_all_regions(Check):
# Determine overall status
issues = []
if not has_delegated_admin:
if not admin_lookup_failed and not has_delegated_admin:
issues.append("no delegated administrator configured")
if not detector_enabled:
issues.append("detector not enabled")
@@ -57,6 +66,22 @@ class guardduty_delegated_admin_enabled_all_regions(Check):
f"GuardDuty in region {detector.region} has issues: "
f"{', '.join(issues)}."
)
if admin_lookup_failed:
report.status_extended = (
f"{report.status_extended[:-1]}; the delegated administrator "
f"status could not be determined."
)
elif admin_lookup_failed:
# Not being able to read the delegated administrator is a lack of
# visibility, not a misconfiguration: the API is only available to
# the management or delegated administrator account.
report.status = "MANUAL"
report.status_extended = (
f"GuardDuty delegated administrator status in region "
f"{detector.region} could not be determined; run this check "
f"from the organization management or delegated administrator "
f"account."
)
else:
report.status = "PASS"
report.status_extended = (
@@ -14,6 +14,7 @@ class GuardDuty(AWSService):
super().__init__(__class__.__name__, provider)
self.detectors = []
self.organization_admin_accounts = []
self.organization_admin_lookup_failed_regions: set = set()
self.__threading_call__(self._list_detectors)
self.__threading_call__(self._get_detector, self.detectors)
self._list_findings()
@@ -227,6 +228,9 @@ class GuardDuty(AWSService):
This API is only available to the organization management account or
a delegated administrator account.
Args:
regional_client: Regional client object.
"""
logger.info("GuardDuty - listing organization admin accounts...")
try:
@@ -235,12 +239,30 @@ class GuardDuty(AWSService):
)
for page in paginator.paginate():
for admin in page.get("AdminAccounts", []):
# GuardDuty returns AdminAccountId/AdminStatus, unlike Security
# Hub's AccountId/Status for the same operation name.
account_id = admin.get("AdminAccountId")
status = admin.get("AdminStatus")
if not account_id or not status:
# An entry we cannot interpret means the delegated admin
# status for this region is unknown, not absent.
if (
regional_client.region
not in self.organization_admin_lookup_failed_regions
):
logger.warning(
f"{regional_client.region} -- Unexpected admin account entry with keys {sorted(admin)}"
)
self.organization_admin_lookup_failed_regions.add(
regional_client.region
)
continue
admin_account = OrganizationAdminAccount(
admin_account_id=admin.get("AdminAccountId"),
admin_status=admin.get("AdminStatus"),
admin_account_id=account_id,
admin_status=status,
region=regional_client.region,
)
# Avoid duplicates across regions for the same admin account
# Avoid duplicates across pages for the same admin account
if not any(
existing.admin_account_id == admin_account.admin_account_id
and existing.region == admin_account.region
@@ -248,6 +270,7 @@ class GuardDuty(AWSService):
):
self.organization_admin_accounts.append(admin_account)
except ClientError as error:
self.organization_admin_lookup_failed_regions.add(regional_client.region)
if error.response["Error"]["Code"] in (
"AccessDeniedException",
"BadRequestException",
@@ -260,6 +283,7 @@ class GuardDuty(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
self.organization_admin_lookup_failed_regions.add(regional_client.region)
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@@ -27,7 +27,6 @@ class securityhub_delegated_admin_enabled_all_regions(Check):
for admin in securityhub_client.organization_admin_accounts
if admin.admin_status == "ENABLED"
}
admin_lookup_failed = securityhub_client.organization_admin_lookup_failed
for securityhub in securityhub_client.securityhubs:
report = Check_Report_AWS(metadata=self.metadata(), resource=securityhub)
@@ -35,6 +34,15 @@ class securityhub_delegated_admin_enabled_all_regions(Check):
# Check if this region has a delegated admin
has_delegated_admin = securityhub.region in regions_with_admin
# The lookup is tracked per region so that a failure in one region does
# not mask the result of the others. A partial failure is only relevant
# when no delegated admin was found: if one was, the status is known.
admin_lookup_failed = (
not has_delegated_admin
and securityhub.region
in securityhub_client.organization_admin_lookup_failed_regions
)
# Check if hub is active
hub_active = securityhub.status == "ACTIVE"
@@ -43,9 +51,7 @@ class securityhub_delegated_admin_enabled_all_regions(Check):
# Determine overall status
issues = []
if admin_lookup_failed:
issues.append("delegated administrator status could not be determined")
elif not has_delegated_admin:
if not admin_lookup_failed and not has_delegated_admin:
issues.append("no delegated administrator configured")
if not hub_active:
issues.append("Security Hub not enabled")
@@ -64,6 +70,22 @@ class securityhub_delegated_admin_enabled_all_regions(Check):
f"Security Hub in region {securityhub.region} has issues: "
f"{', '.join(issues)}."
)
if admin_lookup_failed:
report.status_extended = (
f"{report.status_extended[:-1]}; the delegated administrator "
f"status could not be determined."
)
elif admin_lookup_failed:
# Not being able to read the delegated administrator is a lack of
# visibility, not a misconfiguration: the API is only available to
# the management or delegated administrator account.
report.status = "MANUAL"
report.status_extended = (
f"Security Hub delegated administrator status in region "
f"{securityhub.region} could not be determined; run this check "
f"from the organization management or delegated administrator "
f"account."
)
else:
report.status = "PASS"
report.status_extended = (
@@ -14,7 +14,7 @@ class SecurityHub(AWSService):
super().__init__(__class__.__name__, provider)
self.securityhubs = []
self.organization_admin_accounts = []
self.organization_admin_lookup_failed: bool = False
self.organization_admin_lookup_failed_regions: set = set()
self.__threading_call__(self._describe_hub)
self.__threading_call__(self._list_tags, self.securityhubs)
self.__threading_call__(self._list_organization_admin_accounts)
@@ -115,6 +115,9 @@ class SecurityHub(AWSService):
This API is only available to the organization management account or
a delegated administrator account.
Args:
regional_client: Regional client object.
"""
logger.info("SecurityHub - listing organization admin accounts...")
try:
@@ -123,12 +126,30 @@ class SecurityHub(AWSService):
)
for page in paginator.paginate():
for admin in page.get("AdminAccounts", []):
# Security Hub returns AccountId/Status, unlike GuardDuty's
# AdminAccountId/AdminStatus for the same operation name.
account_id = admin.get("AccountId")
status = admin.get("Status")
if not account_id or not status:
# An entry we cannot interpret means the delegated admin
# status for this region is unknown, not absent.
if (
regional_client.region
not in self.organization_admin_lookup_failed_regions
):
logger.warning(
f"{regional_client.region} -- Unexpected admin account entry with keys {sorted(admin)}"
)
self.organization_admin_lookup_failed_regions.add(
regional_client.region
)
continue
admin_account = OrganizationAdminAccount(
admin_account_id=admin.get("AdminAccountId"),
admin_status=admin.get("AdminStatus"),
admin_account_id=account_id,
admin_status=status,
region=regional_client.region,
)
# Avoid duplicates across regions for the same admin account
# Avoid duplicates across pages for the same admin account
if not any(
existing.admin_account_id == admin_account.admin_account_id
and existing.region == admin_account.region
@@ -136,7 +157,7 @@ class SecurityHub(AWSService):
):
self.organization_admin_accounts.append(admin_account)
except ClientError as error:
self.organization_admin_lookup_failed = True
self.organization_admin_lookup_failed_regions.add(regional_client.region)
if error.response["Error"]["Code"] in (
"AccessDeniedException",
"InvalidAccessException",
@@ -150,7 +171,7 @@ class SecurityHub(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
self.organization_admin_lookup_failed = True
self.organization_admin_lookup_failed_regions.add(regional_client.region)
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
+41
View File
@@ -27,3 +27,44 @@ def is_builtin_provider(provider: str) -> bool:
return spec is not None
except (ImportError, ValueError):
return False
def builtin_check_module(provider: str, service: str, check_name: str) -> str:
"""Return the module path a built-in check would live at."""
return f"prowler.providers.{provider}.services.{service}.{check_name}.{check_name}"
def is_builtin_check(provider: str, service: str, check_name: str) -> bool:
"""Return True if the check's module ships with the SDK.
Sibling of `is_builtin_provider`, and unsafe for the same reason if probed
naively: `find_spec` imports the parent package in order to search it, so
asking about a check that lives in a plug-in raises `ModuleNotFoundError`
rather than returning `None`. A check registered through
`prowler.checks.{provider}` never has a parent under
`prowler.providers.{provider}.services.{service}`, so the naive probe makes
every external check on a built-in provider unresolvable.
Unlike its sibling this one narrows the exception instead of swallowing
every `ImportError`. A provider either ships with the SDK or it does not,
but callers rely on this probe to tell "the check is not built-in" apart
from "the check is built-in and its imports are broken". Reporting the
second as the first would turn a broken dependency into a silent
"check not found".
"""
module = builtin_check_module(provider, service, check_name)
try:
return importlib.util.find_spec(module) is not None
except ModuleNotFoundError as error:
# Only absorb "this check is simply not here". `error.name` is the
# module that could not be imported; when it is the check's own path
# (or a prefix of it) the check does not ship with the SDK. Anything
# else — a missing third-party dependency, say — belongs to a built-in
# check that does exist and must stay loud.
if error.name is None or (
error.name != module and not module.startswith(f"{error.name}.")
):
raise
return False
except ValueError:
return False
@@ -1,40 +1,40 @@
from prowler.exceptions.exceptions import ProwlerException
# Exceptions codes from 19000 to 19099 are reserved for Huawei Cloud exceptions
# Exceptions codes from 20000 to 20999 are reserved for Huawei Cloud exceptions
class HuaweiCloudBaseException(ProwlerException):
"""Base class for Huawei Cloud errors."""
HUAWEICLOUD_ERROR_CODES = {
(19000, "HuaweiCloudCredentialsError"): {
(20000, "HuaweiCloudCredentialsError"): {
"message": "Huawei Cloud credentials not found or invalid",
"remediation": "Provide valid Huawei Cloud credentials via the HUAWEICLOUD_ACCESS_KEY_ID and HUAWEICLOUD_SECRET_ACCESS_KEY environment variables.",
},
(19001, "HuaweiCloudAuthenticationError"): {
(20001, "HuaweiCloudAuthenticationError"): {
"message": "Huawei Cloud authentication failed",
"remediation": "Verify the Access Key ID, Secret Access Key and Project/Domain ID, and ensure the credentials have the required IAM read permissions.",
},
(19002, "HuaweiCloudSetUpSessionError"): {
(20002, "HuaweiCloudSetUpSessionError"): {
"message": "Huawei Cloud session setup failed",
"remediation": "Review the Huawei Cloud SDK initialization parameters and credentials.",
},
(19003, "HuaweiCloudIdentityError"): {
(20003, "HuaweiCloudIdentityError"): {
"message": "Unable to retrieve Huawei Cloud identity or account information",
"remediation": "Ensure the credentials allow access to the IAM Keystone APIs (list auth domains/projects and show user).",
},
(19004, "HuaweiCloudInvalidRegionError"): {
(20004, "HuaweiCloudInvalidRegionError"): {
"message": "One or more requested Huawei Cloud regions are invalid",
"remediation": "Pass a valid Huawei Cloud region id to --region. See https://developer.huaweicloud.com/intl/en-us/endpoint for the current list.",
},
(19005, "HuaweiCloudInvalidProviderIdError"): {
(20005, "HuaweiCloudInvalidProviderIdError"): {
"message": "The provided Huawei Cloud account id does not match the authenticated account",
"remediation": "Ensure the credentials belong to the expected Huawei Cloud account id.",
},
(19006, "HuaweiCloudServiceError"): {
(20006, "HuaweiCloudServiceError"): {
"message": "Huawei Cloud service error",
"remediation": "Review the requested service and region, and check the Huawei Cloud API documentation for more details.",
},
(19007, "HuaweiCloudAssumeRoleError"): {
(20007, "HuaweiCloudAssumeRoleError"): {
"message": "Failed to assume the Huawei Cloud agency",
"remediation": "Verify HUAWEICLOUD_AGENCY_NAME and the target account (HUAWEICLOUD_ASSUME_DOMAIN_ID or HUAWEICLOUD_ASSUME_DOMAIN_NAME), and ensure the agency delegates the required permissions to the authenticated account.",
},
@@ -65,7 +65,7 @@ class HuaweiCloudCredentialsError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19000, file=file, original_exception=original_exception, message=message
20000, file=file, original_exception=original_exception, message=message
)
@@ -74,7 +74,7 @@ class HuaweiCloudAuthenticationError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19001, file=file, original_exception=original_exception, message=message
20001, file=file, original_exception=original_exception, message=message
)
@@ -83,7 +83,7 @@ class HuaweiCloudSetUpSessionError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19002, file=file, original_exception=original_exception, message=message
20002, file=file, original_exception=original_exception, message=message
)
@@ -92,7 +92,7 @@ class HuaweiCloudIdentityError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19003, file=file, original_exception=original_exception, message=message
20003, file=file, original_exception=original_exception, message=message
)
@@ -101,7 +101,7 @@ class HuaweiCloudInvalidRegionError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19004, file=file, original_exception=original_exception, message=message
20004, file=file, original_exception=original_exception, message=message
)
@@ -110,7 +110,7 @@ class HuaweiCloudInvalidProviderIdError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19005, file=file, original_exception=original_exception, message=message
20005, file=file, original_exception=original_exception, message=message
)
@@ -119,7 +119,7 @@ class HuaweiCloudServiceError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19006, file=file, original_exception=original_exception, message=message
20006, file=file, original_exception=original_exception, message=message
)
@@ -128,5 +128,5 @@ class HuaweiCloudAssumeRoleError(HuaweiCloudBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
19007, file=file, original_exception=original_exception, message=message
20007, file=file, original_exception=original_exception, message=message
)
@@ -1,7 +1,7 @@
from prowler.exceptions.exceptions import ProwlerException
# Exceptions codes from 5000 to 5999 are reserved for M365 exceptions
# Exceptions codes from 6000 to 6999 are reserved for M365 exceptions
class M365BaseException(ProwlerException):
"""Base class for M365 Errors."""
@@ -106,25 +106,24 @@ class Identity(OCIService):
identity_client, user.id, compartment.id
)
capabilities = getattr(user, "capabilities", None)
# Check if user can use API keys
can_use_api_keys = (
user.capabilities.can_use_api_keys
if hasattr(user, "capabilities")
else True
can_use_api_keys = getattr(
capabilities, "can_use_api_keys", None
)
if can_use_api_keys is None:
can_use_api_keys = True
# Check if console password is enabled
can_use_console_password = (
user.capabilities.can_use_console_password
if hasattr(user, "capabilities")
else False
getattr(capabilities, "can_use_console_password", None)
or False
)
# Check MFA status
is_mfa_activated = (
user.is_mfa_activated
if hasattr(user, "is_mfa_activated")
else False
getattr(user, "is_mfa_activated", None) or False
)
self.users.append(
@@ -132,19 +131,11 @@ class Identity(OCIService):
id=user.id,
name=user.name,
description=(
user.description or ""
if hasattr(user, "description")
else ""
),
email=(
user.email or ""
if hasattr(user, "email")
else ""
getattr(user, "description", None) or ""
),
email=(getattr(user, "email", None) or ""),
email_verified=(
user.email_verified
if hasattr(user, "email_verified")
else False
getattr(user, "email_verified", None) or False
),
compartment_id=compartment.id,
time_created=user.time_created,
@@ -207,9 +198,7 @@ class Identity(OCIService):
auth_tokens.append(
AuthToken(
id=token.id,
description=(
token.description if hasattr(token, "description") else ""
),
description=(getattr(token, "description", None) or ""),
lifecycle_state=token.lifecycle_state,
time_created=token.time_created,
time_expires=(
@@ -239,9 +228,7 @@ class Identity(OCIService):
customer_secret_keys.append(
CustomerSecretKey(
id=key.id,
display_name=(
key.display_name if hasattr(key, "display_name") else ""
),
display_name=(getattr(key, "display_name", None) or ""),
lifecycle_state=key.lifecycle_state,
time_created=key.time_created,
time_expires=(
@@ -335,9 +322,7 @@ class Identity(OCIService):
id=group.id,
name=group.name,
description=(
group.description
if hasattr(group, "description")
else ""
getattr(group, "description", None) or ""
),
compartment_id=compartment.id,
time_created=group.time_created,
@@ -379,9 +364,7 @@ class Identity(OCIService):
id=policy.id,
name=policy.name,
description=(
policy.description
if hasattr(policy, "description")
else ""
getattr(policy, "description", None) or ""
),
compartment_id=compartment.id,
statements=policy.statements,
@@ -424,15 +407,11 @@ class Identity(OCIService):
id=dynamic_group.id,
name=dynamic_group.name,
description=(
dynamic_group.description or ""
if hasattr(dynamic_group, "description")
else ""
getattr(dynamic_group, "description", None) or ""
),
compartment_id=self.audited_tenancy,
matching_rule=(
dynamic_group.matching_rule
if hasattr(dynamic_group, "matching_rule")
else ""
getattr(dynamic_group, "matching_rule", None) or ""
),
time_created=dynamic_group.time_created,
lifecycle_state=dynamic_group.lifecycle_state,
+22 -18
View File
@@ -68,7 +68,7 @@ dependencies = [
"boto3==1.40.61",
"botocore==1.40.61",
"colorama==0.4.6",
"cryptography==46.0.7",
"cryptography==50.0.0",
"dash==3.1.1",
"dash-bootstrap-components==2.0.3",
"defusedxml==0.7.1",
@@ -80,13 +80,13 @@ dependencies = [
"kubernetes==32.0.1",
"linode-api4==5.45.0",
"markdown==3.10.2",
"microsoft-kiota-abstractions==1.9.9",
"microsoft-kiota-abstractions==1.9.10",
"numpy==2.2.6",
"msgraph-sdk==1.55.0",
"okta==3.4.2",
"openstacksdk==4.2.0",
"pandas==2.2.3",
"py-ocsf-models==0.8.1",
"py-ocsf-models==0.10.0",
"pydantic==2.12.5",
"pygithub==2.8.0",
"python-dateutil==2.9.0.post0",
@@ -103,10 +103,10 @@ dependencies = [
"uuid6==2024.7.10",
"py-iam-expand==0.3.0",
"h2==4.3.0",
"oci==2.169.0",
"oci==2.183.0",
"alibabacloud_credentials==1.0.3",
"alibabacloud_ram20150501==1.2.0",
"alibabacloud_tea_openapi==0.4.4",
"alibabacloud_tea_openapi==0.4.5",
"alibabacloud_sts20150401==1.1.6",
"alibabacloud_vpc20160428==6.13.0",
"alibabacloud_ecs20140526==7.2.5",
@@ -136,7 +136,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">=3.10,<3.14"
version = "5.37.0"
version = "5.37.2"
[project.scripts]
prowler = "prowler.__main__:prowler"
@@ -199,7 +199,7 @@ constraint-dependencies = [
"alibabacloud-sas20181203==6.1.0",
"alibabacloud-sts20150401==1.1.6",
"alibabacloud-tea==0.4.3",
"alibabacloud-tea-openapi==0.4.4",
"alibabacloud-tea-openapi==0.4.5",
"alibabacloud-tea-util==0.3.14",
"alibabacloud-tea-xml==0.0.3",
"alibabacloud-vpc20160428==6.13.0",
@@ -228,7 +228,7 @@ constraint-dependencies = [
"click-plugins==1.1.1.2",
"contextlib2==21.6.0",
"coverage==7.6.12",
"darabonba-core==1.0.5",
"darabonba-core==1.0.8",
"decorator==5.2.1",
"deprecated==1.3.1",
"dill==0.4.1",
@@ -253,7 +253,7 @@ constraint-dependencies = [
"h11==0.16.0",
"hpack==4.1.0",
"httpcore==1.0.9",
"httplib2==0.31.2",
"httplib2==0.32.0",
"httpx==0.28.1",
"huaweicloudsdkcore==3.1.204",
"huaweicloudsdkcts==3.1.204",
@@ -277,7 +277,7 @@ constraint-dependencies = [
"itsdangerous==2.2.0",
"jinja2==3.1.6",
"jmespath==1.1.0",
"joserfc==1.6.5",
"joserfc==1.6.8",
"jsonpatch==1.33",
"jsonpath-ng==1.8.0",
"jsonpointer==3.1.1",
@@ -291,12 +291,12 @@ constraint-dependencies = [
"markupsafe==3.0.3",
"mccabe==0.7.0",
"mdurl==0.1.2",
"microsoft-kiota-authentication-azure==1.9.9",
"microsoft-kiota-http==1.9.9",
"microsoft-kiota-serialization-form==1.9.9",
"microsoft-kiota-serialization-json==1.9.9",
"microsoft-kiota-serialization-multipart==1.9.9",
"microsoft-kiota-serialization-text==1.9.9",
"microsoft-kiota-authentication-azure==1.9.10",
"microsoft-kiota-http==1.9.10",
"microsoft-kiota-serialization-form==1.9.10",
"microsoft-kiota-serialization-json==1.9.10",
"microsoft-kiota-serialization-multipart==1.9.10",
"microsoft-kiota-serialization-text==1.9.10",
"mock==5.2.0",
"moto==5.1.11",
"mpmath==1.3.0",
@@ -331,7 +331,7 @@ constraint-dependencies = [
"protobuf==7.34.1",
"psutil==7.2.2",
"py-partiql-parser==0.6.1",
"pyasn1==0.6.3",
"pyasn1==0.6.4",
"pyasn1-modules==0.4.2",
"pycodestyle==2.12.1",
"pycparser==3.0",
@@ -389,7 +389,11 @@ constraint-dependencies = [
"zipp==3.23.1",
"zstd==1.5.7.3"
]
override-dependencies = ["okta==3.4.2"]
override-dependencies = [
"okta==3.4.2",
# alibabacloud-tea-openapi 0.4.5 caps cryptography below 49 and is the latest release.
"cryptography==50.0.0",
]
[tool.vulture]
# Suppress known false positives. The CI command only passes --exclude and
+135
View File
@@ -5,6 +5,7 @@ from datetime import datetime
from time import mktime
import pytest
import yaml
from mock import patch
from prowler.lib.utils.utils import (
@@ -17,6 +18,7 @@ from prowler.lib.utils.utils import (
open_file,
outputs_unix_timestamp,
parse_json_file,
secrets_rules_path,
strip_ansi_codes,
validate_ip_address,
)
@@ -259,6 +261,139 @@ class Test_detect_secrets_scan_batch:
assert "x" in results
JDBC_RULE = "JDBC connection string with embedded credentials"
class Test_detect_secrets_scan_batch_jdbc:
"""The bundled override of Kingfisher's built-in ``kingfisher.jdbc.1``.
The built-in rule matches a bare ``jdbc:<scheme>:`` prefix followed by any 10
non-space characters, so every JDBC connection string was reported as an
embedded credential. The override in
``prowler/lib/utils/kingfisher_rules/kingfisher_jdbc_1.yaml`` requires an
actual credential; these tests pin both halves of that behavior.
"""
def _jdbc_findings(self, connection_string):
results = detect_secrets_scan_batch({"a": connection_string})
return [f for f in results.get("a", []) if f["type"] == JDBC_RULE]
def test_override_keeps_every_non_pattern_field_of_the_builtin(self):
"""Replacing the built-in rule drops any field the override omits.
Losing ``validation`` would silently stop ``--scan-secrets-validate``
from confirming a JDBC credential is live, and losing
``pattern_requirements`` would stop placeholder values being discarded
neither of which any behavioral test would catch. Only ``pattern`` and
``examples`` are meant to diverge.
"""
with open(
os.path.join(secrets_rules_path, "kingfisher_jdbc_1.yaml"),
encoding="utf-8",
) as f:
rule = yaml.safe_load(f)["rules"][0]
# Verbatim from crates/kingfisher-rules/data/rules/jdbc.yml upstream.
assert rule["id"] == "kingfisher.jdbc.1"
assert rule["name"] == JDBC_RULE
assert rule["confidence"] == "medium"
assert rule["min_entropy"] == 3.3
assert rule["validation"] == {"type": "Jdbc"}
assert rule["tls_mode"] == "lax"
assert rule["pattern_requirements"] == {
"min_special_chars": 2,
"special_chars": ";=/?@&",
"ignore_if_contains": ["****", "xxxx", "example"],
}
assert rule["references"]
def test_rules_path_is_passed_to_kingfisher(self):
"""The override is only in effect if the directory is actually shipped
and handed to Kingfisher."""
assert os.path.isdir(secrets_rules_path)
assert os.path.isfile(
os.path.join(secrets_rules_path, "kingfisher_jdbc_1.yaml")
)
with patch(
"prowler.lib.utils.utils.subprocess.run",
side_effect=_fake_kingfisher_run(output_content="{}"),
) as mocked_run:
detect_secrets_scan_batch({"a": "data"})
command = mocked_run.call_args[0][0]
assert "--rules-path" in command
assert command[command.index("--rules-path") + 1] == secrets_rules_path
@pytest.mark.parametrize(
"connection_string",
[
"jdbc:postgresql://mydb.cluster-abc123.eu-west-1.rds.amazonaws.com:5432/appdb", # trufflehog:ignore
"jdbc:oracle:thin:@ora.corp.internal:1521/ORCLPDB1", # trufflehog:ignore
"jdbc:oracle:thin:@//ora.corp.internal:1521/SVC", # trufflehog:ignore
"jdbc:mysql://prod.internal:3306/inventory?useSSL=true", # trufflehog:ignore
"jdbc:sqlserver://sql.corp.internal:1433;databaseName=inv;integratedSecurity=true", # trufflehog:ignore
"jdbc:redshift://cluster.abc.us-east-1.redshift.amazonaws.com:5439/dev", # trufflehog:ignore
# A username alone is not a credential.
"jdbc:mysql://prod.internal:3306/inventory?user=admin", # trufflehog:ignore
# An empty password is not a credential.
"jdbc:postgresql://pg.corp.internal/app?password=", # trufflehog:ignore
"jdbc:mysql://(host=db.internal,user=alice,password=)/app", # trufflehog:ignore
"jdbc:mysql://address=(host=db.internal)(user=alice)(password=)/app", # trufflehog:ignore
# Connector/J host-list credentials require a non-empty username.
"jdbc:mysql://(host=db.internal,user=,password=Zq81ncPl42)/app", # trufflehog:ignore
"jdbc:mysql://address=(host=db.internal)(user=)(password=Zq81ncPl42)/app", # trufflehog:ignore
# Connector/J host-list syntax must not apply to other drivers.
"jdbc:postgresql://(host=db.internal,user=alice,password=Zq81ncPl42)/app", # trufflehog:ignore
# An `@` in the query string must not turn the host and port into
# `user:password`: without the userinfo alternative being anchored
# to `//`, `db.internal:3306?user=alice` reads as a credential.
"jdbc:mysql://db.internal:3306?user=alice@corp.internal", # trufflehog:ignore
# The same backtrack against the `user/password@` alternative.
"jdbc:mysql://db.internal:3306?owner=team/ops@corp.internal", # trufflehog:ignore
"jdbc:mysql://db.internal:3306?path=a:b/c@corp.internal", # trufflehog:ignore
# And against a `;`-delimited property list.
"jdbc:sqlserver://sql.corp.internal:1433;user=sa@corp.internal", # trufflehog:ignore
# `user/password@` is Oracle TNS syntax and a credential only after
# an Oracle prefix. Every other subprotocol reads `a/b@c` as part of
# a path or a host, so the alternative must not apply to them.
"jdbc:derby:team/ops@corp.internal", # trufflehog:ignore
"jdbc:sqlite:team/ops@corp.internal", # trufflehog:ignore
"jdbc:h2:file:team/ops@corp.internal", # trufflehog:ignore
# The exact payload shape of a CloudFormation Output
# ("OutputKey:OutputValue"), which is how this was reported.
"DatabaseUrl:jdbc:postgresql://mydb.eu-west-1.rds.amazonaws.com:5432/appdb", # trufflehog:ignore
],
)
def test_credential_free_connection_string_is_not_reported(self, connection_string):
assert self._jdbc_findings(connection_string) == []
@pytest.mark.parametrize(
"connection_string",
[
# URL userinfo.
"jdbc:mysql://admin:s3cr3t@prod.internal:3306/inventory", # trufflehog:ignore
# MySQL Connector/J host-list credentials.
"jdbc:mysql://(host=db.internal,user=alice,password=Zq81ncPl42)/app", # trufflehog:ignore
"jdbc:mysql://address=(host=db.internal)(user=alice)(password=Zq81ncPl42)/app", # trufflehog:ignore
# Password as a query parameter.
"jdbc:postgresql://pg.corp.internal:5432/app?user=admin&password=Tr0ub4dor3", # trufflehog:ignore
"jdbc:postgresql://pg.corp.internal/app?password=Xk29fjWa02", # trufflehog:ignore
"jdbc:mysql://prod.internal/db?user=a&pwd=Zq81ncPl42", # trufflehog:ignore
# Password as a semicolon-delimited property.
"jdbc:sqlserver://sql.corp.internal:1433;databaseName=inv;user=sa;password=S3cr3t99", # trufflehog:ignore
"jdbc:sqlserver://sql.corp.internal:1433;Password=Vb73msQr18;user=sa", # trufflehog:ignore
# Oracle TNS userinfo, for each driver type.
"jdbc:oracle:thin:scott/tiger99@ora.corp.internal:1521:ORCL", # trufflehog:ignore
"jdbc:oracle:oci:scott/tiger99@ora.corp.internal:1521:ORCL", # trufflehog:ignore
# Two-character scheme, which the built-in pattern could not match.
"jdbc:h2:file:./data/store;CIPHER=AES;PASSWORD=Nf62kdTp07", # trufflehog:ignore
],
)
def test_embedded_credential_is_still_reported(self, connection_string):
assert self._jdbc_findings(connection_string) != []
class Test_detect_secrets_scan_batch_failures:
"""A scanner failure must surface as SecretsScanError, never as empty
results (which a caller would read as 'no secrets found')."""
@@ -1,10 +1,16 @@
import os
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, call, patch
import pytest
from alibabacloud_tea_openapi.exceptions import ClientException
from darabonba.core import DaraCore
from darabonba.exceptions import RetryError, UnretryableException
from Tea.exceptions import UnretryableException as TeaUnretryableException
from Tea.response import TeaResponse
from prowler.providers.alibabacloud.alibabacloud_provider import AlibabacloudProvider
from prowler.providers.alibabacloud.exceptions.exceptions import (
AlibabaCloudConnectionError,
AlibabaCloudInvalidCredentialsError,
AlibabaCloudSetUpSessionError,
)
@@ -12,9 +18,243 @@ from prowler.providers.alibabacloud.models import AlibabaCloudCallerIdentity
from prowler.providers.common.models import Connection
def test_validate_credentials_non_authentication_api_error_is_not_invalid_credentials():
"""Test non-authentication STS API errors are not relabeled as credentials."""
session = MagicMock()
session.get_credentials.return_value = MagicMock(
access_key_id="LTAI1234567890",
access_key_secret="test-secret-key",
security_token=None,
)
api_error = ClientException(
code="Forbidden",
message="The caller is not authorized",
status_code=403,
)
wrapped_api_error = TeaUnretryableException(request=None, ex=api_error)
with (
patch.object(DaraCore, "do_action", side_effect=wrapped_api_error),
patch.object(DaraCore, "sleep") as sleep,
):
with pytest.raises(UnretryableException) as exception:
AlibabacloudProvider.validate_credentials(session)
assert not isinstance(exception.value, AlibabaCloudInvalidCredentialsError)
assert exception.value.inner_exception is wrapped_api_error
assert exception.value.inner_exception.inner_exception is api_error
sleep.assert_not_called()
def test_validate_credentials_retries_transport_failure_then_succeeds():
"""Test STS caller identity retries a transient transport failure."""
session = MagicMock()
session.get_credentials.return_value = MagicMock(
access_key_id="LTAI1234567890",
access_key_secret="test-secret-key",
security_token=None,
)
response = TeaResponse()
response.status_code = 200
response.headers = {"content-type": "application/json"}
response.body = (
b'{"AccountId":"1234567890","PrincipalId":"123456",'
b'"Arn":"acs:ram::1234567890:user/test-user",'
b'"IdentityType":"RamUser"}'
)
with (
patch.object(
DaraCore,
"do_action",
side_effect=[RetryError("connection reset"), response],
) as do_action,
patch.object(DaraCore, "sleep") as sleep,
):
caller_identity = AlibabacloudProvider.validate_credentials(session)
assert caller_identity.account_id == "1234567890"
assert do_action.call_count == 2
sleep.assert_called_once_with(1000)
def test_validate_credentials_connection_failure_is_not_invalid_credentials():
"""Test exhausted STS transport retries raise a connection-specific error."""
session = MagicMock()
session.get_credentials.return_value = MagicMock(
access_key_id="LTAI1234567890",
access_key_secret="test-secret-key",
security_token=None,
)
retry_errors = []
for _ in range(3):
connection_reset = ConnectionResetError(104, "Connection reset by peer")
retry_error = RetryError(str(connection_reset))
retry_error.__cause__ = connection_reset
retry_errors.append(retry_error)
with (
patch.object(DaraCore, "do_action", side_effect=retry_errors) as do_action,
patch.object(DaraCore, "sleep") as sleep,
):
with pytest.raises(AlibabaCloudConnectionError) as exception:
AlibabacloudProvider.validate_credentials(session)
assert not isinstance(exception.value, AlibabaCloudInvalidCredentialsError)
assert do_action.call_count == 3
assert sleep.call_args_list == [call(1000), call(1000)]
assert isinstance(exception.value.original_exception, UnretryableException)
assert exception.value.original_exception.inner_exception is retry_errors[-1]
assert exception.value.__cause__ is exception.value.original_exception
def test_validate_credentials_genuine_invalid_credentials():
"""Test an explicit STS authentication failure remains a credentials error."""
session = MagicMock()
session.get_credentials.return_value = MagicMock(
access_key_id="LTAI-invalid",
access_key_secret="invalid-secret",
security_token=None,
)
authentication_error = ClientException(
code="InvalidAccessKeyId.NotFound",
message="The AccessKey ID does not exist",
status_code=400,
)
wrapped_authentication_error = TeaUnretryableException(
request=None, ex=authentication_error
)
with (
patch.object(DaraCore, "do_action", side_effect=wrapped_authentication_error),
patch.object(DaraCore, "sleep") as sleep,
):
with pytest.raises(AlibabaCloudInvalidCredentialsError) as exception:
AlibabacloudProvider.validate_credentials(session)
assert isinstance(exception.value.original_exception, UnretryableException)
assert (
exception.value.original_exception.inner_exception
is wrapped_authentication_error
)
assert (
exception.value.original_exception.inner_exception.inner_exception
is authentication_error
)
assert exception.value.__cause__ is exception.value.original_exception
sleep.assert_not_called()
def test_validate_credentials_authentication_error_wins_over_transport_error():
"""Test a definitive nested authentication error takes precedence over transport."""
session = MagicMock()
session.get_credentials.return_value = MagicMock(
access_key_id="LTAI-invalid",
access_key_secret="invalid-secret",
security_token=None,
)
authentication_error = ClientException(
code="InvalidAccessKeyId.NotFound",
message="The AccessKey ID does not exist",
status_code=400,
)
retry_errors = []
for _ in range(3):
retry_error = RetryError("connection reset")
retry_error.__cause__ = authentication_error
retry_errors.append(retry_error)
with (
patch.object(DaraCore, "do_action", side_effect=retry_errors),
patch.object(DaraCore, "sleep") as sleep,
):
with pytest.raises(AlibabaCloudInvalidCredentialsError) as exception:
AlibabacloudProvider.validate_credentials(session)
assert isinstance(exception.value.original_exception, UnretryableException)
assert exception.value.original_exception.inner_exception is retry_errors[-1]
assert exception.value.__cause__ is exception.value.original_exception
assert sleep.call_args_list == [call(1000), call(1000)]
class TestAlibabacloudProviderTestConnection:
"""Tests for the AlibabacloudProvider.test_connection method."""
def test_test_connection_connection_error_no_raise(self):
"""Test connection failures are returned with their dedicated type."""
mock_session = MagicMock()
connection_error = AlibabaCloudConnectionError(
file="test_file",
original_exception=RetryError("connection reset"),
)
with (
patch.object(
AlibabacloudProvider,
"setup_session",
return_value=mock_session,
),
patch.object(
AlibabacloudProvider,
"validate_credentials",
side_effect=connection_error,
),
patch(
"prowler.providers.alibabacloud.alibabacloud_provider.logger.error"
) as logger_error,
patch(
"prowler.providers.alibabacloud.alibabacloud_provider.logger.critical"
) as logger_critical,
):
result = AlibabacloudProvider.test_connection(
access_key_id="LTAI1234567890",
access_key_secret="test-secret-key",
raise_on_exception=False,
)
assert result.is_connected is False
assert result.error is connection_error
assert result.error.code == 10008
logger_error.assert_called_once()
logger_critical.assert_not_called()
def test_test_connection_connection_error_raises(self):
"""Test connection failures retain raise-on-exception behavior."""
mock_session = MagicMock()
connection_error = AlibabaCloudConnectionError(
file="test_file",
original_exception=RetryError("connection reset"),
)
with (
patch.object(
AlibabacloudProvider,
"setup_session",
return_value=mock_session,
),
patch.object(
AlibabacloudProvider,
"validate_credentials",
side_effect=connection_error,
),
patch(
"prowler.providers.alibabacloud.alibabacloud_provider.logger.error"
) as logger_error,
patch(
"prowler.providers.alibabacloud.alibabacloud_provider.logger.critical"
) as logger_critical,
):
with pytest.raises(AlibabaCloudConnectionError) as exception:
AlibabacloudProvider.test_connection(
access_key_id="LTAI1234567890",
access_key_secret="test-secret-key",
raise_on_exception=True,
)
assert exception.value is connection_error
logger_error.assert_called_once()
logger_critical.assert_not_called()
def test_test_connection_with_static_credentials_success(self):
"""Test successful connection with static access key credentials."""
mock_session = MagicMock()
+5 -1
View File
@@ -5,6 +5,7 @@ Mocks Alibaba Cloud SDK modules to avoid import issues when the real
dependencies are not installed in the test environment.
"""
import importlib
import sys
from unittest.mock import MagicMock
@@ -38,4 +39,7 @@ MOCKED_MODULES = [
]
for module_name in MOCKED_MODULES:
sys.modules.setdefault(module_name, MagicMock())
try:
importlib.import_module(module_name)
except ModuleNotFoundError:
sys.modules.setdefault(module_name, MagicMock())
@@ -430,10 +430,15 @@ class Test_config_delegated_admin_and_org_aggregator_all_regions:
assert eu_west_1_result is not None
# The check still runs; aggregator coverage is satisfied but the
# delegated-admin status is unknown, so it must FAIL.
assert eu_west_1_result.status == "FAIL"
# delegated-admin status is unknown, which is a lack of visibility
# rather than a misconfiguration.
assert eu_west_1_result.status == "MANUAL"
assert (
"delegated administrator status for config.amazonaws.com could not be determined"
"delegated administrator status for config.amazonaws.com could "
"not be determined" in eu_west_1_result.status_extended
)
assert (
"organization management or delegated administrator account"
in eu_west_1_result.status_extended
)
@@ -7,56 +7,114 @@ from moto import mock_aws
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
mocked_api_response,
set_mocked_aws_provider,
)
orig = botocore.client.BaseClient._make_api_call
def mocked_response(operation_name, response):
"""Validate a mocked GuardDuty response against the real API model."""
return mocked_api_response("guardduty", operation_name, response)
def mock_make_api_call_org_admin_and_config(self, operation_name, api_params):
"""Mock organization admin accounts and configuration APIs."""
if operation_name == "ListOrganizationAdminAccounts":
return {
"AdminAccounts": [
{
"AdminAccountId": "123456789012",
"AdminStatus": "ENABLED",
}
]
}
return mocked_response(
operation_name,
{
"AdminAccounts": [
{"AdminAccountId": "123456789012", "AdminStatus": "ENABLED"}
]
},
)
if operation_name == "DescribeOrganizationConfiguration":
return {
"AutoEnableOrganizationMembers": "ALL",
}
return mocked_response(
operation_name,
{
"AutoEnableOrganizationMembers": "ALL",
"MemberAccountLimitReached": False,
},
)
return orig(self, operation_name, api_params)
def mock_make_api_call_org_admin_no_auto_enable(self, operation_name, api_params):
"""Mock organization admin configured but auto-enable disabled."""
if operation_name == "ListOrganizationAdminAccounts":
return {
"AdminAccounts": [
{
"AdminAccountId": "123456789012",
"AdminStatus": "ENABLED",
}
]
}
return mocked_response(
operation_name,
{
"AdminAccounts": [
{"AdminAccountId": "123456789012", "AdminStatus": "ENABLED"}
]
},
)
if operation_name == "DescribeOrganizationConfiguration":
return {
"AutoEnableOrganizationMembers": "NONE",
}
return mocked_response(
operation_name,
{
"AutoEnableOrganizationMembers": "NONE",
"MemberAccountLimitReached": False,
},
)
return orig(self, operation_name, api_params)
def mock_make_api_call_no_org_admin(self, operation_name, api_params):
"""Mock no organization admin configured."""
if operation_name == "ListOrganizationAdminAccounts":
return {"AdminAccounts": []}
return mocked_response(operation_name, {"AdminAccounts": []})
if operation_name == "DescribeOrganizationConfiguration":
return {
"AutoEnableOrganizationMembers": "NONE",
}
return mocked_response(
operation_name,
{
"AutoEnableOrganizationMembers": "NONE",
"MemberAccountLimitReached": False,
},
)
return orig(self, operation_name, api_params)
def mock_make_api_call_admin_lookup_access_denied(self, operation_name, api_params):
"""ListOrganizationAdminAccounts is denied — lookup-failed path."""
if operation_name == "ListOrganizationAdminAccounts":
raise botocore.exceptions.ClientError(
{
"Error": {
"Code": "AccessDeniedException",
"Message": "User is not authorized to perform: guardduty:ListOrganizationAdminAccounts",
}
},
operation_name,
)
if operation_name == "DescribeOrganizationConfiguration":
return mocked_response(
operation_name,
{
"AutoEnableOrganizationMembers": "ALL",
"MemberAccountLimitReached": False,
},
)
return orig(self, operation_name, api_params)
def mock_make_api_call_admin_account_missing_fields(self, operation_name, api_params):
"""AdminAccounts entry without the documented fields."""
if operation_name == "ListOrganizationAdminAccounts":
# Deliberately not validated against the API model: this simulates the
# response drifting away from what botocore currently describes.
return {"AdminAccounts": [{"SomethingElse": "unexpected"}]}
if operation_name == "DescribeOrganizationConfiguration":
return mocked_response(
operation_name,
{
"AutoEnableOrganizationMembers": "ALL",
"MemberAccountLimitReached": False,
},
)
return orig(self, operation_name, api_params)
@@ -231,3 +289,67 @@ class Test_guardduty_delegated_admin_enabled_all_regions:
eu_west_1_result.resource_arn
== f"arn:aws:guardduty:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:detector/{detector_id}"
)
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_lookup_access_denied,
)
@mock_aws
def test_admin_lookup_access_denied(self):
"""A denied lookup is unknown, not absent: MANUAL instead of FAIL."""
guardduty_client_boto = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client_boto.create_detector(Enable=True)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty
service = GuardDuty(aws_provider)
assert AWS_REGION_EU_WEST_1 in service.organization_admin_lookup_failed_regions
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
patch(
"prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions.guardduty_client",
new=service,
),
):
from prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions import (
guardduty_delegated_admin_enabled_all_regions,
)
result = guardduty_delegated_admin_enabled_all_regions().execute()
assert result and result[0].status == "MANUAL"
assert "could not be determined" in result[0].status_extended
assert (
"management or delegated administrator account"
in result[0].status_extended
)
assert (
"no delegated administrator configured" not in result[0].status_extended
)
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_account_missing_fields,
)
@mock_aws
def test_admin_account_missing_fields(self):
"""An unparseable admin entry marks the region as unknown instead of raising."""
guardduty_client_boto = client("guardduty", region_name=AWS_REGION_EU_WEST_1)
guardduty_client_boto.create_detector(Enable=True)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty
service = GuardDuty(aws_provider)
assert service.organization_admin_accounts == []
assert service.organization_admin_lookup_failed_regions == {
AWS_REGION_EU_WEST_1
}
@@ -6,6 +6,8 @@ from moto import mock_aws
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
AWS_REGION_EU_WEST_2,
mocked_api_response,
set_mocked_aws_provider,
)
@@ -14,6 +16,11 @@ orig = botocore.client.BaseClient._make_api_call
HUB_ARN = f"arn:aws:securityhub:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:hub/default"
def mocked_response(operation_name, response):
"""Validate a mocked Security Hub response against the real API model."""
return mocked_api_response("securityhub", operation_name, response)
def _active_hub_responses(operation_name):
"""Return a moto-friendly response for hub-describing API calls.
@@ -21,17 +28,20 @@ def _active_hub_responses(operation_name):
can fall back to the default behavior).
"""
if operation_name == "DescribeHub":
return {
"HubArn": HUB_ARN,
"SubscribedAt": "2024-01-01T00:00:00.000Z",
"AutoEnableControls": True,
}
return mocked_response(
operation_name,
{
"HubArn": HUB_ARN,
"SubscribedAt": "2024-01-01T00:00:00.000Z",
"AutoEnableControls": True,
},
)
if operation_name == "GetEnabledStandards":
return {"StandardsSubscriptions": []}
return mocked_response(operation_name, {"StandardsSubscriptions": []})
if operation_name == "ListEnabledProductsForImport":
return {"ProductSubscriptions": []}
return mocked_response(operation_name, {"ProductSubscriptions": []})
if operation_name == "ListTagsForResource":
return {"Tags": {}}
return mocked_response(operation_name, {"Tags": {}})
return None
@@ -41,19 +51,14 @@ def mock_make_api_call_org_admin_and_config(self, operation_name, api_params):
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
return {
"AdminAccounts": [
{
"AdminAccountId": "123456789012",
"AdminStatus": "ENABLED",
}
]
}
return mocked_response(
operation_name,
{"AdminAccounts": [{"AccountId": "123456789012", "Status": "ENABLED"}]},
)
if operation_name == "DescribeOrganizationConfiguration":
return {
"AutoEnable": True,
"AutoEnableStandards": "DEFAULT",
}
return mocked_response(
operation_name, {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
)
return orig(self, operation_name, api_params)
@@ -63,19 +68,14 @@ def mock_make_api_call_org_admin_no_auto_enable(self, operation_name, api_params
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
return {
"AdminAccounts": [
{
"AdminAccountId": "123456789012",
"AdminStatus": "ENABLED",
}
]
}
return mocked_response(
operation_name,
{"AdminAccounts": [{"AccountId": "123456789012", "Status": "ENABLED"}]},
)
if operation_name == "DescribeOrganizationConfiguration":
return {
"AutoEnable": False,
"AutoEnableStandards": "NONE",
}
return mocked_response(
operation_name, {"AutoEnable": False, "AutoEnableStandards": "NONE"}
)
return orig(self, operation_name, api_params)
@@ -85,12 +85,11 @@ def mock_make_api_call_no_org_admin(self, operation_name, api_params):
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
return {"AdminAccounts": []}
return mocked_response(operation_name, {"AdminAccounts": []})
if operation_name == "DescribeOrganizationConfiguration":
return {
"AutoEnable": False,
"AutoEnableStandards": "NONE",
}
return mocked_response(
operation_name, {"AutoEnable": False, "AutoEnableStandards": "NONE"}
)
return orig(self, operation_name, api_params)
@@ -107,7 +106,7 @@ def mock_make_api_call_securityhub_not_subscribed(self, operation_name, api_para
operation_name,
)
if operation_name == "ListOrganizationAdminAccounts":
return {"AdminAccounts": []}
return mocked_response(operation_name, {"AdminAccounts": []})
return orig(self, operation_name, api_params)
@@ -127,7 +126,9 @@ def mock_make_api_call_admin_lookup_access_denied(self, operation_name, api_para
operation_name,
)
if operation_name == "DescribeOrganizationConfiguration":
return {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
return mocked_response(
operation_name, {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
)
return orig(self, operation_name, api_params)
@@ -139,7 +140,9 @@ def mock_make_api_call_admin_lookup_unexpected(self, operation_name, api_params)
if operation_name == "ListOrganizationAdminAccounts":
raise RuntimeError("simulated transient error")
if operation_name == "DescribeOrganizationConfiguration":
return {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
return mocked_response(
operation_name, {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
)
return orig(self, operation_name, api_params)
@@ -151,11 +154,10 @@ def mock_make_api_call_describe_org_config_other_client_error(
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
return {
"AdminAccounts": [
{"AdminAccountId": "123456789012", "AdminStatus": "ENABLED"}
]
}
return mocked_response(
operation_name,
{"AdminAccounts": [{"AccountId": "123456789012", "Status": "ENABLED"}]},
)
if operation_name == "DescribeOrganizationConfiguration":
raise botocore.exceptions.ClientError(
{"Error": {"Code": "InternalServerError", "Message": "boom"}},
@@ -170,28 +172,92 @@ def mock_make_api_call_describe_org_config_unexpected(self, operation_name, api_
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
return {
"AdminAccounts": [
{"AdminAccountId": "123456789012", "AdminStatus": "ENABLED"}
]
}
return mocked_response(
operation_name,
{"AdminAccounts": [{"AccountId": "123456789012", "Status": "ENABLED"}]},
)
if operation_name == "DescribeOrganizationConfiguration":
raise RuntimeError("simulated transient error")
return orig(self, operation_name, api_params)
def mock_make_api_call_admin_account_missing_fields(self, operation_name, api_params):
"""AdminAccounts entry without the expected keys — must not raise."""
hub_resp = _active_hub_responses(operation_name)
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
# Deliberately not validated against the API model: this simulates the
# response drifting away from what botocore currently describes.
return {"AdminAccounts": [{"SomethingElse": "unexpected"}]}
if operation_name == "DescribeOrganizationConfiguration":
return mocked_response(
operation_name, {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
)
return orig(self, operation_name, api_params)
def mock_make_api_call_admin_account_partially_unparseable(
self, operation_name, api_params
):
"""A valid admin account alongside an entry that cannot be parsed."""
hub_resp = _active_hub_responses(operation_name)
if hub_resp is not None:
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
# Deliberately not validated against the API model: the second entry
# simulates the response drifting away from what botocore describes.
return {
"AdminAccounts": [
{"AccountId": "123456789012", "Status": "ENABLED"},
{"SomethingElse": "unexpected"},
]
}
if operation_name == "DescribeOrganizationConfiguration":
return mocked_response(
operation_name, {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
)
return orig(self, operation_name, api_params)
def mock_make_api_call_admin_lookup_denied_in_one_region(
self, operation_name, api_params
):
"""ListOrganizationAdminAccounts fails only in eu-west-2."""
hub_resp = _active_hub_responses(operation_name)
if hub_resp is not None:
if operation_name == "DescribeHub":
return mocked_response(
operation_name,
{
**hub_resp,
"HubArn": f"arn:aws:securityhub:{self.meta.region_name}:{AWS_ACCOUNT_NUMBER}:hub/default",
},
)
return hub_resp
if operation_name == "ListOrganizationAdminAccounts":
if self.meta.region_name == AWS_REGION_EU_WEST_2:
raise botocore.exceptions.ClientError(
{
"Error": {
"Code": "AccessDeniedException",
"Message": "User is not authorized to perform: securityhub:ListOrganizationAdminAccounts",
}
},
operation_name,
)
return mocked_response(
operation_name,
{"AdminAccounts": [{"AccountId": "123456789012", "Status": "ENABLED"}]},
)
if operation_name == "DescribeOrganizationConfiguration":
return mocked_response(
operation_name, {"AutoEnable": True, "AutoEnableStandards": "DEFAULT"}
)
return orig(self, operation_name, api_params)
class Test_securityhub_delegated_admin_enabled_all_regions:
def teardown_method(self):
"""Evict cached securityhub modules so legacy mock.patch-based tests
in the same session see a fresh import path."""
import sys
for mod in (
"prowler.providers.aws.services.securityhub.securityhub_client",
"prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions",
):
sys.modules.pop(mod, None)
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_securityhub_not_subscribed,
@@ -395,9 +461,10 @@ class Test_securityhub_delegated_admin_enabled_all_regions:
break
assert eu_west_1_result is not None
assert eu_west_1_result.status == "FAIL"
assert eu_west_1_result.status == "MANUAL"
assert (
"delegated administrator status could not be determined"
"could not be determined" in eu_west_1_result.status_extended
and "management or delegated administrator account"
in eu_west_1_result.status_extended
)
assert (
@@ -419,7 +486,7 @@ class Test_securityhub_delegated_admin_enabled_all_regions:
)
service = SecurityHub(aws_provider)
assert service.organization_admin_lookup_failed is True
assert AWS_REGION_EU_WEST_1 in service.organization_admin_lookup_failed_regions
with (
patch(
@@ -436,11 +503,8 @@ class Test_securityhub_delegated_admin_enabled_all_regions:
)
result = securityhub_delegated_admin_enabled_all_regions().execute()
assert result and result[0].status == "FAIL"
assert (
"delegated administrator status could not be determined"
in result[0].status_extended
)
assert result and result[0].status == "MANUAL"
assert "could not be determined" in result[0].status_extended
@patch(
"botocore.client.BaseClient._make_api_call",
@@ -510,3 +574,112 @@ class Test_securityhub_delegated_admin_enabled_all_regions:
result = securityhub_delegated_admin_enabled_all_regions().execute()
assert result and result[0].status == "PASS"
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_account_missing_fields,
)
@mock_aws
def test_admin_account_missing_fields(self):
"""An unparseable admin entry is reported as unknown, not as absent."""
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
from prowler.providers.aws.services.securityhub.securityhub_service import (
SecurityHub,
)
service = SecurityHub(aws_provider)
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
patch(
"prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions.securityhub_client",
new=service,
),
):
from prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions import (
securityhub_delegated_admin_enabled_all_regions,
)
result = securityhub_delegated_admin_enabled_all_regions().execute()
assert result and result[0].status == "MANUAL"
assert "could not be determined" in result[0].status_extended
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_lookup_denied_in_one_region,
)
@mock_aws
def test_admin_lookup_failure_is_isolated_per_region(self):
"""A lookup failure in one region must not degrade the other regions."""
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
)
from prowler.providers.aws.services.securityhub.securityhub_service import (
SecurityHub,
)
service = SecurityHub(aws_provider)
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
patch(
"prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions.securityhub_client",
new=service,
),
):
from prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions import (
securityhub_delegated_admin_enabled_all_regions,
)
results = {
finding.region: finding
for finding in securityhub_delegated_admin_enabled_all_regions().execute()
}
assert results[AWS_REGION_EU_WEST_1].status == "PASS"
assert results[AWS_REGION_EU_WEST_2].status == "MANUAL"
assert (
"could not be determined"
in results[AWS_REGION_EU_WEST_2].status_extended
)
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_account_partially_unparseable,
)
@mock_aws
def test_admin_account_partially_unparseable(self):
"""A known delegated admin is not downgraded to unknown by a partial failure."""
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
from prowler.providers.aws.services.securityhub.securityhub_service import (
SecurityHub,
)
service = SecurityHub(aws_provider)
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
patch(
"prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions.securityhub_client",
new=service,
),
):
from prowler.providers.aws.services.securityhub.securityhub_delegated_admin_enabled_all_regions.securityhub_delegated_admin_enabled_all_regions import (
securityhub_delegated_admin_enabled_all_regions,
)
result = securityhub_delegated_admin_enabled_all_regions().execute()
assert result and result[0].status == "PASS"
assert "could not be determined" not in result[0].status_extended
@@ -1,34 +1,80 @@
from unittest import mock
from moto import mock_aws
from prowler.providers.aws.services.securityhub.securityhub_service import (
SecurityHubHub,
)
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1
# Patching the fixer client imports securityhub_client, which instantiates
# SecurityHub against the global provider at module level. Stubbing the class
# first keeps that import from reaching AWS when this file runs on its own.
SERVICE_MODULE = "prowler.providers.aws.services.securityhub.securityhub_service"
FIXER_MODULE = "prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled_fixer"
class test_securityhub_enabled_fixer:
@mock_aws
def _mocked_securityhub_client(fixer_config: dict) -> tuple:
"""Build a Security Hub client mock with a single regional client."""
regional_client = mock.MagicMock()
securityhub_client = mock.MagicMock()
securityhub_client.fixer_config = fixer_config
securityhub_client.regional_clients = {AWS_REGION_EU_WEST_1: regional_client}
return securityhub_client, regional_client
class Test_securityhub_enabled_fixer:
def test_securityhub_enabled_fixer(self):
securityhub_client = mock.MagicMock
securityhub_client.securityhubs = [
SecurityHubHub(
arn="arn:aws:securityhub:us-east-1:0123456789012:hub/default",
id="default",
status="ACTIVE",
standards="cis-aws-foundations-benchmark/v/1.2.0",
integrations="",
region="eu-west-1",
)
]
with mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub",
new=securityhub_client,
"""Security Hub is enabled with the default standards from the fixer config."""
securityhub_client, regional_client = _mocked_securityhub_client(
{"securityhub_enabled": {"EnableDefaultStandards": True}}
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{FIXER_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Check
# Test Fixer
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled_fixer import (
fixer,
)
assert fixer(AWS_REGION_EU_WEST_1)
regional_client.enable_security_hub.assert_called_once_with(
EnableDefaultStandards=True
)
def test_securityhub_enabled_fixer_default_standards_disabled(self):
"""EnableDefaultStandards must be taken from the fixer configuration."""
securityhub_client, regional_client = _mocked_securityhub_client(
{"securityhub_enabled": {"EnableDefaultStandards": False}}
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{FIXER_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Fixer
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled_fixer import (
fixer,
)
assert fixer(AWS_REGION_EU_WEST_1)
regional_client.enable_security_hub.assert_called_once_with(
EnableDefaultStandards=False
)
def test_securityhub_enabled_fixer_error(self):
"""A failing EnableSecurityHub call must return False instead of raising."""
securityhub_client, regional_client = _mocked_securityhub_client({})
regional_client.enable_security_hub.side_effect = Exception(
"AccessDeniedException"
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{FIXER_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Fixer
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled_fixer import (
fixer,
)
assert not fixer(AWS_REGION_EU_WEST_1)
@@ -3,36 +3,52 @@ from unittest import mock
from prowler.providers.aws.services.securityhub.securityhub_service import (
SecurityHubHub,
)
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1
# Patching the check client imports securityhub_client, which instantiates
# SecurityHub against the global provider at module level. Stubbing the class
# first keeps that import from reaching AWS when this file runs on its own.
SERVICE_MODULE = "prowler.providers.aws.services.securityhub.securityhub_service"
CHECK_MODULE = (
"prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled"
)
HUB_ARN = f"arn:aws:securityhub:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:hub/default"
UNKNOWN_HUB_ARN = (
f"arn:aws:securityhub:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:hub/unknown"
)
def _mocked_securityhub_client(securityhubs: list, audit_config: dict = None):
"""Build a Security Hub client mock holding the given hubs."""
securityhub_client = mock.MagicMock()
securityhub_client.region = AWS_REGION_EU_WEST_1
securityhub_client.audited_partition = "aws"
securityhub_client.audited_account = AWS_ACCOUNT_NUMBER
securityhub_client.audit_config = audit_config if audit_config is not None else {}
securityhub_client.securityhubs = securityhubs
return securityhub_client
class Test_securityhub_enabled:
def test_securityhub_hub_inactive(self):
securityhub_client = mock.MagicMock
securityhub_client.region = AWS_REGION_EU_WEST_1
securityhub_client.get_unknown_arn = (
lambda x: f"arn:aws:securityhub:{x}:0123456789012:hub/unknown"
securityhub_client = _mocked_securityhub_client(
[
SecurityHubHub(
arn=UNKNOWN_HUB_ARN,
id="hub/unknown",
status="NOT_AVAILABLE",
standards="",
integrations="",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
)
securityhub_client.securityhubs = [
SecurityHubHub(
arn=f"arn:aws:securityhub:{AWS_REGION_EU_WEST_1}:0123456789012:hub/unknown",
id="hub/unknown",
status="NOT_AVAILABLE",
standards="",
integrations="",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
with (
mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub",
new=securityhub_client,
),
mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub.get_unknown_arn",
return_value="arn:aws:securityhub:eu-west-1:0123456789012:hub/unknown",
),
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{CHECK_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Check
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import (
@@ -42,32 +58,32 @@ class Test_securityhub_enabled:
check = securityhub_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Security Hub is not enabled."
assert result[0].resource_id == "hub/unknown"
assert (
result[0].resource_arn
== "arn:aws:securityhub:eu-west-1:0123456789012:hub/unknown"
)
assert result[0].resource_arn == UNKNOWN_HUB_ARN
assert result[0].region == AWS_REGION_EU_WEST_1
assert result[0].resource_tags == [{"test_key": "test_value"}]
def test_securityhub_hub_active_with_standards(self):
securityhub_client = mock.MagicMock
securityhub_client.securityhubs = [
SecurityHubHub(
arn="arn:aws:securityhub:us-east-1:0123456789012:hub/default",
id="default",
status="ACTIVE",
standards="cis-aws-foundations-benchmark/v/1.2.0",
integrations="",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
with mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub",
new=securityhub_client,
securityhub_client = _mocked_securityhub_client(
[
SecurityHubHub(
arn=HUB_ARN,
id="default",
status="ACTIVE",
standards="cis-aws-foundations-benchmark/v/1.2.0",
integrations="",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{CHECK_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Check
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import (
@@ -77,35 +93,35 @@ class Test_securityhub_enabled:
check = securityhub_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Security Hub is enabled with standards: cis-aws-foundations-benchmark/v/1.2.0."
)
assert result[0].resource_id == "default"
assert (
result[0].resource_arn
== "arn:aws:securityhub:us-east-1:0123456789012:hub/default"
)
assert result[0].resource_arn == HUB_ARN
assert result[0].region == AWS_REGION_EU_WEST_1
assert result[0].resource_tags == [{"test_key": "test_value"}]
def test_securityhub_hub_active_with_integrations(self):
securityhub_client = mock.MagicMock
securityhub_client.securityhubs = [
SecurityHubHub(
arn="arn:aws:securityhub:us-east-1:0123456789012:hub/default",
id="default",
status="ACTIVE",
standards="",
integrations="prowler",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
with mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub",
new=securityhub_client,
securityhub_client = _mocked_securityhub_client(
[
SecurityHubHub(
arn=HUB_ARN,
id="default",
status="ACTIVE",
standards="",
integrations="prowler",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{CHECK_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Check
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import (
@@ -115,38 +131,35 @@ class Test_securityhub_enabled:
check = securityhub_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Security Hub is enabled without standards but with integrations: prowler."
)
assert result[0].resource_id == "default"
assert (
result[0].resource_arn
== "arn:aws:securityhub:us-east-1:0123456789012:hub/default"
)
assert result[0].resource_arn == HUB_ARN
assert result[0].region == AWS_REGION_EU_WEST_1
assert result[0].resource_tags == [{"test_key": "test_value"}]
def test_securityhub_hub_active_without_integrations_or_standards(self):
securityhub_client = mock.MagicMock
securityhub_client.region = AWS_REGION_EU_WEST_1
securityhub_client.audited_partition = "aws"
securityhub_client.audited_account = "0123456789012"
securityhub_client.securityhubs = [
SecurityHubHub(
arn="arn:aws:securityhub:us-east-1:0123456789012:hub/default",
id="default",
status="ACTIVE",
standards="",
integrations="",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
with mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub",
new=securityhub_client,
securityhub_client = _mocked_securityhub_client(
[
SecurityHubHub(
arn=HUB_ARN,
id="default",
status="ACTIVE",
standards="",
integrations="",
region=AWS_REGION_EU_WEST_1,
tags=[{"test_key": "test_value"}],
)
]
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{CHECK_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Check
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import (
@@ -156,39 +169,37 @@ class Test_securityhub_enabled:
check = securityhub_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert not result[0].muted
assert (
result[0].status_extended
== "Security Hub is enabled but without any standard or integration."
)
assert result[0].resource_id == "default"
assert (
result[0].resource_arn
== "arn:aws:securityhub:us-east-1:0123456789012:hub/default"
)
assert result[0].resource_arn == HUB_ARN
assert result[0].region == AWS_REGION_EU_WEST_1
assert result[0].resource_tags == [{"test_key": "test_value"}]
def test_securityhub_hub_active_without_integrations_or_standards_muted(self):
securityhub_client = mock.MagicMock
securityhub_client.audit_config = {"mute_non_default_regions": True}
securityhub_client.region = AWS_REGION_EU_WEST_1
securityhub_client.audited_partition = "aws"
securityhub_client.audited_account = "0123456789012"
securityhub_client.securityhubs = [
SecurityHubHub(
arn="arn:aws:securityhub:us-east-1:0123456789012:hub/default",
id="default",
status="ACTIVE",
standards="",
integrations="",
region="eu-south-2",
tags=[],
)
]
with mock.patch(
"prowler.providers.aws.services.securityhub.securityhub_service.SecurityHub",
new=securityhub_client,
securityhub_client = _mocked_securityhub_client(
[
SecurityHubHub(
arn=HUB_ARN,
id="default",
status="ACTIVE",
standards="",
integrations="",
region="eu-south-2",
tags=[],
)
],
audit_config={"mute_non_default_regions": True},
)
with (
mock.patch(f"{SERVICE_MODULE}.SecurityHub", new=mock.MagicMock()),
mock.patch(f"{CHECK_MODULE}.securityhub_client", new=securityhub_client),
):
# Test Check
from prowler.providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import (
@@ -198,6 +209,7 @@ class Test_securityhub_enabled:
check = securityhub_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].muted
assert (
@@ -205,9 +217,6 @@ class Test_securityhub_enabled:
== "Security Hub is enabled but without any standard or integration."
)
assert result[0].resource_id == "default"
assert (
result[0].resource_arn
== "arn:aws:securityhub:us-east-1:0123456789012:hub/default"
)
assert result[0].resource_arn == HUB_ARN
assert result[0].region == "eu-south-2"
assert result[0].resource_tags == []
@@ -3,7 +3,12 @@ from unittest.mock import patch
import botocore
from prowler.providers.aws.services.securityhub.securityhub_service import SecurityHub
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
mocked_api_response,
set_mocked_aws_provider,
)
# Mocking Access Analyzer Calls
make_api_call = botocore.client.BaseClient._make_api_call
@@ -41,10 +46,44 @@ def mock_make_api_call(self, operation_name, kwarg):
return {
"Tags": {"test_key": "test_value"},
}
if operation_name == "ListOrganizationAdminAccounts":
# Security Hub returns AccountId/Status, unlike GuardDuty's
# AdminAccountId/AdminStatus for the same operation name.
return mocked_api_response(
"securityhub",
operation_name,
{"AdminAccounts": [{"AccountId": AWS_ACCOUNT_NUMBER, "Status": "ENABLED"}]},
)
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_admin_account_missing_fields(self, operation_name, kwarg):
"""Return an admin account entry without the documented fields."""
if operation_name == "ListOrganizationAdminAccounts":
# Deliberately not validated against the API model: this simulates the
# response drifting away from what botocore currently describes.
return {"AdminAccounts": [{"SomethingElse": "unexpected"}]}
return mock_make_api_call(self, operation_name, kwarg)
def mock_make_api_call_admin_account_access_denied(self, operation_name, kwarg):
"""Deny ListOrganizationAdminAccounts, as AWS does outside the management account."""
if operation_name == "ListOrganizationAdminAccounts":
raise botocore.exceptions.ClientError(
{
"Error": {
"Code": "AccessDeniedException",
"Message": "User is not authorized to perform: securityhub:ListOrganizationAdminAccounts",
}
},
operation_name,
)
return mock_make_api_call(self, operation_name, kwarg)
# Mock generate_regional_clients()
def mock_generate_regional_clients(provider, service):
regional_client = provider._session.current_session.client(
@@ -91,3 +130,46 @@ class Test_SecurityHub_Service:
securityhub = SecurityHub(set_mocked_aws_provider([AWS_REGION_EU_WEST_1]))
assert len(securityhub.securityhubs) == 1
assert securityhub.securityhubs[0].tags == [{"test_key": "test_value"}]
def test_list_organization_admin_accounts(self):
"""Security Hub returns AccountId/Status, not GuardDuty's AdminAccountId/AdminStatus."""
securityhub = SecurityHub(set_mocked_aws_provider([AWS_REGION_EU_WEST_1]))
assert securityhub.organization_admin_lookup_failed_regions == set()
assert len(securityhub.organization_admin_accounts) == 1
assert (
securityhub.organization_admin_accounts[0].admin_account_id
== AWS_ACCOUNT_NUMBER
)
assert securityhub.organization_admin_accounts[0].admin_status == "ENABLED"
assert securityhub.organization_admin_accounts[0].region == AWS_REGION_EU_WEST_1
def test_list_organization_admin_accounts_missing_fields(self):
"""An unparseable entry marks the region as unknown instead of raising."""
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_account_missing_fields,
):
securityhub = SecurityHub(aws_provider)
assert securityhub.organization_admin_accounts == []
assert securityhub.organization_admin_lookup_failed_regions == {
AWS_REGION_EU_WEST_1
}
def test_list_organization_admin_accounts_access_denied(self):
"""A denied lookup only marks its own region as unknown."""
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_admin_account_access_denied,
):
securityhub = SecurityHub(aws_provider)
assert securityhub.organization_admin_accounts == []
assert securityhub.organization_admin_lookup_failed_regions == {
AWS_REGION_EU_WEST_1
}
+38
View File
@@ -1,7 +1,10 @@
from argparse import Namespace
from functools import lru_cache
from json import dumps
import botocore.session
from boto3 import client, session
from botocore.validate import ParamValidator
from moto import mock_aws
from prowler.config.config import (
@@ -235,3 +238,38 @@ def create_role(
PolicyArn=policy["Arn"],
)
return administrator_role["Arn"]
@lru_cache(maxsize=None)
def _service_model(service_name: str):
return botocore.session.get_session().get_service_model(service_name)
def mocked_api_response(service_name: str, operation_name: str, response: dict) -> dict:
"""Validate a hand-written mocked response against the real AWS API model.
Responses returned from a `botocore.client.BaseClient._make_api_call` mock are
not validated by botocore, so a mock can return fields that the API never
sends and the test will still pass. Wrapping the response with this helper
turns that silent mismatch into a test failure.
Args:
service_name: Boto3 service name, e.g. `securityhub`.
operation_name: API operation name in PascalCase, e.g. `DescribeHub`.
response: The mocked response to validate and return.
Returns:
The response, unchanged.
Raises:
AssertionError: If the response does not match the operation output shape.
"""
output_shape = (
_service_model(service_name).operation_model(operation_name).output_shape
)
report = ParamValidator().validate(response, output_shape)
assert not report.has_errors(), (
f"Mocked {service_name}:{operation_name} response does not match the API "
f"model: {report.generate_report()}"
)
return response
+119
View File
@@ -0,0 +1,119 @@
from unittest.mock import patch
import pytest
from prowler.providers.common.builtin import (
builtin_check_module,
is_builtin_check,
is_builtin_provider,
)
class TestBuiltinCheckModule:
def test_builds_the_sdk_module_path(self):
assert (
builtin_check_module("aws", "ec2", "ec2_instance_public_ip")
== "prowler.providers.aws.services.ec2.ec2_instance_public_ip.ec2_instance_public_ip"
)
class TestIsBuiltinProvider:
def test_true_for_a_provider_shipped_with_the_sdk(self):
assert is_builtin_provider("aws") is True
def test_false_for_a_provider_that_lives_in_a_plugin(self):
# No `prowler.providers.acme` package: find_spec raises on the absent
# parent rather than returning None, and the helper absorbs it.
assert is_builtin_provider("acme") is False
class TestIsBuiltinCheck:
def test_true_for_a_check_shipped_with_the_sdk(self):
assert is_builtin_check("aws", "ec2", "ec2_instance_public_ip") is True
def test_false_for_an_external_check_on_a_builtin_provider(self):
"""The case that made every plug-in check unresolvable.
`prowler.providers.aws.services.ec2` exists, so the naive probe gets
far enough to try importing the check package as a parent and that
package only exists inside the plug-in. find_spec raises instead of
returning None.
"""
assert (
is_builtin_check("aws", "ec2", "ec2_acme_instance_has_owner_tag") is False
)
def test_false_for_a_service_that_does_not_exist(self):
assert (
is_builtin_check("aws", "acmeservice", "acmeservice_thing_is_fine") is False
)
def test_false_for_an_external_provider(self):
assert (
is_builtin_check("acme", "inventory", "inventory_item_has_owner") is False
)
def test_reraises_when_a_builtin_checks_own_dependency_is_missing(self):
"""A broken import must not read as "the check is not built-in".
Collapsing the two would turn a missing dependency into a silent
"check not found", which is the failure mode this probe exists to
avoid.
"""
module = builtin_check_module("aws", "ec2", "ec2_instance_public_ip")
with patch(
"prowler.providers.common.builtin.importlib.util.find_spec",
side_effect=ModuleNotFoundError("No module named 'boto3'", name="boto3"),
):
with pytest.raises(ModuleNotFoundError):
is_builtin_check("aws", "ec2", "ec2_instance_public_ip")
# Sanity: the same error naming the check's own path is absorbed.
with patch(
"prowler.providers.common.builtin.importlib.util.find_spec",
side_effect=ModuleNotFoundError(f"No module named '{module}'", name=module),
):
assert is_builtin_check("aws", "ec2", "ec2_instance_public_ip") is False
def test_reraises_when_missing_module_name_is_only_a_textual_prefix(self):
"""A sibling module prefix must not read as the check's missing parent."""
sibling_prefix = "prowler.providers.aws.services.ec2.ec2"
with patch(
"prowler.providers.common.builtin.importlib.util.find_spec",
side_effect=ModuleNotFoundError(
f"No module named '{sibling_prefix}'", name=sibling_prefix
),
):
with pytest.raises(ModuleNotFoundError):
is_builtin_check("aws", "ec2", "ec2_instance_public_ip")
@pytest.mark.parametrize(
"error",
[
ValueError("namespace package edge case"),
],
ids=["value_error"],
)
def test_false_when_find_spec_raises_value_error(self, error):
"""Mirrors the guard `is_builtin_provider` already carries.
`find_spec` can fail for reasons that are not "the module is absent"
a namespace-package edge case raises ValueError. That does not say the
check ships with the SDK, so it falls through to the entry points.
"""
with patch(
"prowler.providers.common.builtin.importlib.util.find_spec",
side_effect=error,
):
assert is_builtin_check("aws", "ec2", "ec2_instance_public_ip") is False
def test_reraises_plain_import_error(self):
"""A plain ImportError can indicate a broken built-in check import."""
with patch(
"prowler.providers.common.builtin.importlib.util.find_spec",
side_effect=ImportError("partially initialised"),
):
with pytest.raises(ImportError, match="partially initialised"):
is_builtin_check("aws", "ec2", "ec2_instance_public_ip")
+53 -15
View File
@@ -1333,15 +1333,17 @@ class TestCheckDiscovery:
class TestCheckExecution:
"""Tests 15-17: _resolve_check_module."""
@patch("prowler.lib.check.check.importlib.util.find_spec")
@patch("prowler.lib.check.check.is_builtin_check")
@patch("prowler.lib.check.check.import_check")
def test_resolve_check_module_builtin_first(self, mock_import, mock_find_spec):
def test_resolve_check_module_builtin_first(
self, mock_import, mock_is_builtin_check
):
"""Test 15: _resolve_check_module resolves built-in checks first."""
from prowler.lib.check.check import _resolve_check_module
mock_module = MagicMock()
mock_import.return_value = mock_module
mock_find_spec.return_value = MagicMock() # built-in package exists
mock_is_builtin_check.return_value = True # built-in check exists
result = _resolve_check_module("aws", "ec2", "my_check")
@@ -1350,15 +1352,15 @@ class TestCheckExecution:
"prowler.providers.aws.services.ec2.my_check.my_check"
)
@patch("prowler.lib.check.check.importlib.util.find_spec")
@patch("prowler.lib.check.check.is_builtin_check")
@patch("prowler.lib.check.check.import_check")
def test_resolve_check_module_fallback_to_entry_point(
self, mock_import_check, mock_find_spec
self, mock_import_check, mock_is_builtin_check
):
"""Test 16: _resolve_check_module falls back to entry point when built-in is absent."""
from prowler.lib.check.check import _resolve_check_module
mock_find_spec.return_value = None # built-in does not exist
mock_is_builtin_check.return_value = False # built-in does not exist
mock_ext_module = MagicMock()
ep = _make_entry_point(
@@ -1375,10 +1377,10 @@ class TestCheckExecution:
mock_imp.assert_called_with("ext_pkg.checks.my_check")
mock_import_check.assert_not_called()
@patch("prowler.lib.check.check.importlib.util.find_spec")
@patch("prowler.lib.check.check.is_builtin_check")
@patch("prowler.lib.check.check.import_check")
def test_resolve_check_module_builtin_wins_over_entry_point(
self, mock_import_check, mock_find_spec
self, mock_import_check, mock_is_builtin_check
):
"""Regression guard: when both a built-in and an entry-point check
exist with the same CheckID, the BUILT-IN wins. Plug-ins extend
@@ -1389,7 +1391,7 @@ class TestCheckExecution:
review (HugoPBrito)."""
from prowler.lib.check.check import _resolve_check_module
mock_find_spec.return_value = MagicMock() # built-in exists
mock_is_builtin_check.return_value = True # built-in exists
builtin_module = MagicMock()
mock_import_check.return_value = builtin_module
@@ -1414,21 +1416,23 @@ class TestCheckExecution:
mock_imp.assert_not_called()
@patch("prowler.lib.check.check.importlib.metadata.entry_points")
@patch("prowler.lib.check.check.importlib.util.find_spec")
def test_resolve_check_module_raises_when_not_found(self, mock_find_spec, mock_ep):
@patch("prowler.lib.check.check.is_builtin_check")
def test_resolve_check_module_raises_when_not_found(
self, mock_is_builtin_check, mock_ep
):
"""Test 17: _resolve_check_module raises ModuleNotFoundError when both fail."""
from prowler.lib.check.check import _resolve_check_module
mock_find_spec.return_value = None
mock_is_builtin_check.return_value = False
mock_ep.return_value = []
with pytest.raises(ModuleNotFoundError, match="not found"):
_resolve_check_module("fake", "svc", "nonexistent_check")
@patch("prowler.lib.check.check.importlib.util.find_spec")
@patch("prowler.lib.check.check.is_builtin_check")
@patch("prowler.lib.check.check.import_check")
def test_resolve_check_module_surfaces_error_when_builtin_import_fails(
self, mock_import_check, mock_find_spec
self, mock_import_check, mock_is_builtin_check
):
"""Regression guard: when no plug-in entry-point overrides the
check, a built-in whose module exists but fails to import (e.g.
@@ -1437,7 +1441,7 @@ class TestCheckExecution:
(HugoPBrito)."""
from prowler.lib.check.check import _resolve_check_module
mock_find_spec.return_value = MagicMock() # built-in module exists
mock_is_builtin_check.return_value = True # built-in module exists
mock_import_check.side_effect = ImportError("missing transitive dep: foo")
# No plug-in override — the built-in's import failure must propagate
@@ -1445,6 +1449,40 @@ class TestCheckExecution:
with pytest.raises(ImportError, match="missing transitive dep"):
_resolve_check_module("aws", "ec2", "ec2_instance_public_ip")
def test_resolve_check_module_entry_point_check_on_builtin_provider(self):
"""Regression guard: a plug-in check attached to a BUILT-IN provider.
Deliberately does not mock the built-in probe. The bug this guards
against was invisible to every other test here precisely because they
mock `find_spec` and hand it `None`, while the real call raises: it
imports `prowler.providers.aws.services.ec2.<check>` as the parent it
must search, and that package only exists inside the plug-in. The raw
exception escaped `_resolve_check_module` before the entry points were
ever consulted, so no external check could run against aws, azure, gcp
or any other built-in provider.
"""
from prowler.lib.check.check import _resolve_check_module
mock_module = MagicMock()
ep = _make_entry_point(
"ec2_acme_instance_has_owner_tag",
"acme_checks.services.ec2.ec2_acme_instance_has_owner_tag.ec2_acme_instance_has_owner_tag",
"prowler.checks.aws",
)
with (
patch("importlib.metadata.entry_points", return_value=[ep]),
patch("importlib.import_module", return_value=mock_module) as mock_imp,
):
result = _resolve_check_module(
"aws", "ec2", "ec2_acme_instance_has_owner_tag"
)
assert result is mock_module
mock_imp.assert_called_with(
"acme_checks.services.ec2.ec2_acme_instance_has_owner_tag.ec2_acme_instance_has_owner_tag"
)
# ===========================================================================
# 5. CLI Arguments
@@ -517,7 +517,7 @@ class TestHuaweiCloudExceptions:
for cls in classes:
error = cls(file="huaweicloud_provider.py")
assert isinstance(error, HuaweiCloudBaseException)
assert 19000 <= error.code <= 19099
assert 20000 <= error.code <= 20999
assert error.message
assert error.remediation
codes.add(error.code)
@@ -526,4 +526,4 @@ class TestHuaweiCloudExceptions:
def test_custom_message_override(self):
error = HuaweiCloudServiceError(message="custom service failure")
assert error.message == "custom service failure"
assert error.code == 19006
assert error.code == 20006
@@ -122,6 +122,55 @@ class TestIdentityService:
and all(len(d.password_policies) == 1 for d in identity_client.domains)
)
def test_list_dynamic_groups_with_null_optional_fields(self):
"""OCI can return `matching_rule` and `description` as null; the
dynamic group must still be retrieved instead of failing the whole
listing with a pydantic ValidationError."""
with patch(
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__",
return_value=None,
):
from prowler.providers.oraclecloud.services.identity.identity_service import (
Identity,
)
identity_client = Identity(None)
identity_client.service = "identity"
identity_client.provider = set_mocked_oraclecloud_provider()
identity_client.provider._home_region = "us-ashburn-1"
identity_client.audited_tenancy = "ocid1.tenancy.oc1..aaaaaaaexample"
identity_client.dynamic_groups = []
identity_client.session_signer = None
identity_client.session_config = None
regional_client = MagicMock()
regional_client.region = "us-ashburn-1"
dynamic_group = MagicMock()
dynamic_group.id = "ocid1.dynamicgroup.oc1..aaaaaaaexample"
dynamic_group.name = "prowler-instances"
dynamic_group.description = None
dynamic_group.matching_rule = None
dynamic_group.time_created = datetime.now()
dynamic_group.lifecycle_state = "ACTIVE"
with (
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__",
return_value=MagicMock(),
),
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results",
return_value=MagicMock(data=[dynamic_group]),
),
):
identity_client.__list_dynamic_groups__(regional_client)
assert len(identity_client.dynamic_groups) == 1
assert identity_client.dynamic_groups[0].name == "prowler-instances"
assert identity_client.dynamic_groups[0].matching_rule == ""
assert identity_client.dynamic_groups[0].description == ""
def test_list_domains_concurrent_dedupes_and_prefers_home_region(self):
"""__list_domains__ runs across regions in parallel; the dedupe
must stay correct under concurrent calls (no duplicates, home
+12
View File
@@ -4,6 +4,18 @@ All notable changes to the **Prowler UI** are documented in this file.
<!-- changelog: release notes start -->
## [1.37.1] (Prowler v5.37.1)
### 🐞 Fixed
- Fixed image optimization in the production container: Next.js standalone tracing omitted `sharp`'s native `libvips` library, so every image was served unoptimized [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
### 🔐 Security
- The UI container image now patches musl and zlib alongside OpenSSL, and `sharp` is pinned to 0.35.3, clearing the image's remaining CVEs [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
---
## [1.37.0] (Prowler v5.37.0)
### 🚀 Added
+1 -1
View File
@@ -6,7 +6,7 @@ LABEL maintainer="https://github.com/prowler-cloud"
# Patch Alpine OpenSSL runtime packages before all stages inherit the base image.
# The build uses pnpm via corepack, so npm is unused — remove it (and npx) to drop
# the bundled-npm CVE surface (node-tar CVE-2026-59873) from every stage, incl. prod.
RUN apk upgrade --no-cache libcrypto3 libssl3 && corepack enable && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
RUN apk upgrade --no-cache libcrypto3 libssl3 musl musl-utils zlib && corepack enable && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
# Install dependencies only when needed
FROM base AS deps
+15 -2
View File
@@ -624,9 +624,22 @@ export const handlersForOrganizations = (
}),
// --- launch (scans + schedules) --------------------------------------
http.post(`${API}/scans`, () =>
http.post(`${API}/scans/bulk`, () =>
HttpResponse.json(
{ data: { id: "scan-1", type: "scans", attributes: {} } },
{
data: fx.apply.createdProviderIds.map((providerId, index) => ({
id: `scan-${index + 1}`,
type: "scans",
relationships: {
provider: {
data: { id: providerId, type: "providers" },
},
task: {
data: { id: `scan-task-${index + 1}`, type: "tasks" },
},
},
})),
},
{ status: 202 },
),
),
+8 -2
View File
@@ -2,6 +2,12 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import type {
ComplianceOverviewApiResponse,
ComplianceRequirementsApiResponse,
} from "./types";
export const getCompliancesOverview = async ({
scanId,
@@ -11,7 +17,7 @@ export const getCompliancesOverview = async ({
scanId?: string;
region?: string | string[];
filters?: Record<string, string | string[] | undefined>;
} = {}) => {
} = {}): Promise<ApiResult<ComplianceOverviewApiResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/compliance-overviews`);
@@ -115,7 +121,7 @@ export const getComplianceRequirements = async ({
complianceId: string;
scanId: string;
region?: string | string[];
}) => {
}): Promise<ApiResult<ComplianceRequirementsApiResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
try {
+5
View File
@@ -1,6 +1,11 @@
export * from "./compliances";
export * from "./compliances.adapter";
export { COMPLIANCE_OVERVIEW_RESOURCE_TYPE } from "./types";
export type {
ComplianceOverviewApiResponse,
ComplianceOverviewTaskResource,
ComplianceOverviewTaskResponse,
ComplianceOverviewsResponse,
ComplianceRequirementsApiResponse,
EnrichedComplianceOverview,
} from "./types";
+27 -1
View File
@@ -1,6 +1,14 @@
import { StaticImageData } from "next/image";
import { ComplianceOverviewData } from "@/types/compliance";
import type {
ComplianceOverviewData,
RequirementsData,
} from "@/types/compliance";
import type { TaskAttributes } from "@/types/tasks";
export const COMPLIANCE_OVERVIEW_RESOURCE_TYPE = {
TASK: "tasks",
} as const;
/**
* Raw API response from /compliance-overviews endpoint
@@ -16,6 +24,24 @@ export interface ComplianceOverviewsResponse {
};
}
export interface ComplianceOverviewTaskResource {
id: string;
type: typeof COMPLIANCE_OVERVIEW_RESOURCE_TYPE.TASK;
attributes?: TaskAttributes;
}
export interface ComplianceOverviewTaskResponse {
data: ComplianceOverviewTaskResource;
}
export type ComplianceOverviewApiResponse =
| ComplianceOverviewsResponse
| ComplianceOverviewTaskResponse;
export type ComplianceRequirementsApiResponse =
| RequirementsData
| ComplianceOverviewTaskResponse;
/**
* Enriched compliance overview with computed fields
*/
@@ -1,3 +1,5 @@
import type { ApiResult } from "@/types/server-actions";
import { AttackSurfaceOverview, AttackSurfaceOverviewResponse } from "./types";
const ATTACK_SURFACE_IDS = {
@@ -49,7 +51,7 @@ function mapAttackSurfaceItem(item: AttackSurfaceOverview): AttackSurfaceItem {
* @returns An array of AttackSurfaceItem objects sorted by the predefined order
*/
export function adaptAttackSurfaceOverview(
response: AttackSurfaceOverviewResponse | undefined,
response: ApiResult<AttackSurfaceOverviewResponse> | undefined,
): AttackSurfaceItem[] {
if (!response?.data || response.data.length === 0) {
return [];
@@ -3,6 +3,7 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { AttackSurfaceOverviewResponse } from "./types";
@@ -10,7 +11,7 @@ export const getAttackSurfaceOverview = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<AttackSurfaceOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<AttackSurfaceOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`);
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { adaptComplianceWatchlistResponse } from "./compliance-watchlist.adapter";
import type { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
describe("adaptComplianceWatchlistResponse", () => {
it("returns no items on a 4xx error shape", () => {
// handleApiResponse resolves truthy {error, status} objects for 4xx.
const errorResponse = {
error: "Invalid filter",
status: 400,
} as unknown as ComplianceWatchlistResponse;
expect(adaptComplianceWatchlistResponse(errorResponse)).toEqual([]);
});
it("returns no items on an empty-body success shape", () => {
// handleApiResponse resolves {success, status} for 204 and empty bodies.
const emptyResponse = {
success: true,
status: 204,
} as unknown as ComplianceWatchlistResponse;
expect(adaptComplianceWatchlistResponse(emptyResponse)).toEqual([]);
});
it("returns no items when the fetch failed with undefined", () => {
expect(adaptComplianceWatchlistResponse(undefined)).toEqual([]);
});
});
@@ -1,5 +1,6 @@
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
import { formatLabel } from "@/lib/categories";
import type { ApiResult } from "@/types/server-actions";
import { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
@@ -36,7 +37,7 @@ function formatComplianceLabel(complianceId: string): string {
}
export function adaptComplianceWatchlistResponse(
response: ComplianceWatchlistResponse | undefined,
response: ApiResult<ComplianceWatchlistResponse> | undefined,
): EnrichedComplianceWatchlistItem[] {
if (!response?.data) {
return [];
@@ -3,6 +3,7 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
@@ -10,7 +11,7 @@ export const getComplianceWatchlist = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<ComplianceWatchlistResponse | undefined> => {
} = {}): Promise<ApiResult<ComplianceWatchlistResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/compliance-watchlist`);
+7 -3
View File
@@ -5,8 +5,12 @@ import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { FindingsSeverityOverviewResponse } from "./types";
import {
FindingsSeverityOverviewResponse,
FindingsStatusOverviewResponse,
} from "./types";
export const getFindingsByStatus = async ({
page = 1,
@@ -18,7 +22,7 @@ export const getFindingsByStatus = async ({
query?: string;
sort?: string;
filters?: Record<string, string | string[] | undefined>;
} = {}) => {
} = {}): Promise<ApiResult<FindingsStatusOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
if (isNaN(Number(page)) || page < 1) redirect("/");
@@ -49,7 +53,7 @@ export const getFindingsBySeverity = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<FindingsSeverityOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<FindingsSeverityOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/findings_severity`);
@@ -5,6 +5,34 @@ interface OverviewResponseMeta {
version: string;
}
// Corresponds to the /overviews/findings endpoint (OverviewFindingSerializer)
export interface FindingsStatusAttributes {
new: number;
changed: number;
unchanged: number;
fail_new: number;
fail_changed: number;
pass_new: number;
pass_changed: number;
muted_new: number;
muted_changed: number;
total: number;
pass: number;
fail: number;
muted: number;
}
export interface FindingsStatusOverview {
type: "findings-overview";
id: string;
attributes: FindingsStatusAttributes;
}
export interface FindingsStatusOverviewResponse {
data: FindingsStatusOverview;
meta: OverviewResponseMeta;
}
export interface FindingsSeverityAttributes {
critical: number;
high: number;
+2 -1
View File
@@ -5,6 +5,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 type { ApiResult } from "@/types/server-actions";
import { ProvidersOverviewResponse } from "./types";
@@ -18,7 +19,7 @@ export const getProvidersOverview = async ({
query?: string;
sort?: string;
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<ProvidersOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<ProvidersOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
if (isNaN(Number(page)) || page < 1) redirect("/providers-overview");
+2 -1
View File
@@ -3,6 +3,7 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { RegionsOverviewResponse } from "./types";
@@ -10,7 +11,7 @@ export const getRegionsOverview = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<RegionsOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<RegionsOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/regions`);
@@ -1,4 +1,5 @@
import { getProviderDisplayName } from "@/types/providers";
import type { ApiResult } from "@/types/server-actions";
import { RegionsOverviewResponse } from "./types";
@@ -366,7 +367,7 @@ function formatRegionName(providerType: string, region: string): string {
* Adapts regions overview API response to threat map format.
*/
export function adaptRegionsOverviewToThreatMap(
regionsResponse: RegionsOverviewResponse | undefined,
regionsResponse: ApiResult<RegionsOverviewResponse> | undefined,
): ThreatMapData {
if (!regionsResponse?.data || regionsResponse.data.length === 0) {
return {
@@ -19,6 +19,8 @@ import {
Webhook,
} from "lucide-react";
import type { ApiResult } from "@/types/server-actions";
import {
ResourceGroupOverview,
ResourceGroupOverviewResponse,
@@ -191,7 +193,7 @@ function formatResourceGroupLabel(id: string): string {
* @returns An array of ResourceInventoryItem objects sorted by the predefined order
*/
export function adaptResourceGroupOverview(
response: ResourceGroupOverviewResponse | undefined,
response: ApiResult<ResourceGroupOverviewResponse> | undefined,
): ResourceInventoryItem[] {
if (!response?.data || response.data.length === 0) {
return [];
@@ -3,6 +3,7 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { ResourceGroupOverviewResponse } from "./types";
@@ -10,7 +11,7 @@ export const getResourceGroupOverview = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<ResourceGroupOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<ResourceGroupOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/resource-groups`);
@@ -1,5 +1,6 @@
import type { RadarDataPoint } from "@/components/graphs/types";
import { getCategoryLabel } from "@/lib/categories";
import type { ApiResult } from "@/types/server-actions";
import { CategoryOverview, CategoryOverviewResponse } from "./types";
@@ -44,7 +45,7 @@ function mapCategoryToRadarPoint(item: CategoryOverview): RadarDataPoint {
* @returns An array of RadarDataPoint objects for the radar chart
*/
export function adaptCategoryOverviewToRadarData(
response: CategoryOverviewResponse | undefined,
response: ApiResult<CategoryOverviewResponse> | undefined,
): RadarDataPoint[] {
if (!response?.data || response.data.length === 0) {
return [];
+2 -1
View File
@@ -3,6 +3,7 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { CategoryOverviewResponse } from "./types";
@@ -10,7 +11,7 @@ export const getCategoryOverview = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<CategoryOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<CategoryOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/categories`);
+2 -1
View File
@@ -3,6 +3,7 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { ServicesOverviewResponse } from "./types";
@@ -10,7 +11,7 @@ export const getServicesOverview = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<ServicesOverviewResponse | undefined> => {
} = {}): Promise<ApiResult<ServicesOverviewResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/services`);
@@ -0,0 +1,61 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { fetchMock, getAuthHeadersMock, handleApiResponseMock } = vi.hoisted(
() => ({
fetchMock: vi.fn(),
getAuthHeadersMock: vi.fn(),
handleApiResponseMock: vi.fn(),
}),
);
vi.mock("@/lib", () => ({
apiBaseUrl: "https://api.example.com/api/v1",
getAuthHeaders: getAuthHeadersMock,
}));
vi.mock("@/lib/server-actions-helper", () => ({
handleApiResponse: handleApiResponseMock,
}));
import { getFindingsSeverityTrends } from "./severity-trends";
describe("getFindingsSeverityTrends", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
fetchMock.mockResolvedValue(new Response(null, { status: 200 }));
});
it("returns an error status on a 4xx response shape", async () => {
// handleApiResponse resolves truthy {error, status} objects for 4xx.
handleApiResponseMock.mockResolvedValueOnce({
error: "Invalid filter",
status: 400,
});
const result = await getFindingsSeverityTrends();
expect(result).toEqual({ status: "error" });
});
it("returns an empty status on a no-content response shape", async () => {
// handleApiResponse resolves {success, status} for 204 and empty bodies.
handleApiResponseMock.mockResolvedValueOnce({
success: true,
status: 204,
});
const result = await getFindingsSeverityTrends();
expect(result).toEqual({ status: "empty" });
});
it("returns an empty status when the trend list has no entries", async () => {
handleApiResponseMock.mockResolvedValueOnce({ data: [] });
const result = await getFindingsSeverityTrends();
expect(result).toEqual({ status: "empty" });
});
});
@@ -7,6 +7,7 @@ import {
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import { adaptSeverityTrendsResponse } from "./severity-trends.adapter";
import {
@@ -35,10 +36,16 @@ const getFindingsSeverityTrends = async ({
headers,
});
const apiResponse: FindingsSeverityOverTimeResponse | undefined =
const apiResponse: ApiResult<FindingsSeverityOverTimeResponse> | undefined =
await handleApiResponse(response);
if (!apiResponse?.data || !Array.isArray(apiResponse.data)) {
// 4xx resolves a truthy {error, status} shape — surface it as an error
// instead of misreporting the trend as empty.
if (!apiResponse || "error" in apiResponse) {
return { status: "error" };
}
if (!apiResponse.data || !Array.isArray(apiResponse.data)) {
return { status: "empty" };
}
@@ -3,12 +3,15 @@
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import type { ApiResult } from "@/types/server-actions";
import type { ThreatScoreResponse } from "./types";
export const getThreatScore = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}) => {
} = {}): Promise<ApiResult<ThreatScoreResponse> | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/overviews/threatscore`);
+82 -3
View File
@@ -1,11 +1,13 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const {
addScanOperationMock,
fetchMock,
getAuthHeadersMock,
handleApiErrorMock,
handleApiResponseMock,
} = vi.hoisted(() => ({
addScanOperationMock: vi.fn(),
fetchMock: vi.fn(),
getAuthHeadersMock: vi.fn(),
handleApiErrorMock: vi.fn(),
@@ -27,12 +29,89 @@ vi.mock("@/lib/server-actions-helper", () => ({
}));
vi.mock("@/lib/sentry-breadcrumbs", () => ({
addScanOperation: vi.fn(),
addScanOperation: addScanOperationMock,
}));
import { getExportsZip, launchOrganizationScans } from "./scans";
import {
getExportsZip,
launchOrganizationScans,
scheduleOrganizationDailyScans,
} from "./scans";
describe("launchOrganizationScans", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
handleApiResponseMock.mockResolvedValue({ data: [{ id: "scan-1" }] });
});
it("sends one organization bulk scan request", async () => {
// Given
const scans = [
{ id: "scan-1", type: "scans" },
{ id: "scan-2", type: "scans" },
];
fetchMock.mockResolvedValue(new Response(null, { status: 202 }));
handleApiResponseMock.mockResolvedValue({ data: scans });
// When
const result = await launchOrganizationScans("organization-1");
// Then
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/api/v1/scans/bulk",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
data: {
type: "scans-bulk",
relationships: {
organization: {
data: {
type: "organizations",
id: "organization-1",
},
},
},
},
}),
}),
);
expect(handleApiResponseMock).toHaveBeenCalledWith(
expect.any(Response),
"/scans",
);
expect(result).toEqual({ data: scans });
expect(addScanOperationMock).toHaveBeenCalledTimes(1);
expect(addScanOperationMock).toHaveBeenCalledWith("start", undefined, {
organization_id: "organization-1",
bulk: true,
scan_count: 2,
scan_ids: "scan-1,scan-2",
});
});
it("rejects a successful response without a scan collection", async () => {
// Given
fetchMock.mockResolvedValue(new Response(null, { status: 202 }));
handleApiResponseMock.mockResolvedValue({
data: { id: "scan-1", type: "scans" },
});
// When
const result = await launchOrganizationScans("organization-1");
// Then
expect(result).toEqual({
error: "The bulk scan response did not contain a scan collection.",
});
expect(addScanOperationMock).not.toHaveBeenCalled();
});
});
describe("scheduleOrganizationDailyScans", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
@@ -63,7 +142,7 @@ describe("launchOrganizationScans", () => {
});
// When
const result = await launchOrganizationScans(providerIds, "daily");
const result = await scheduleOrganizationDailyScans(providerIds);
// Then
expect(maxActiveRequests).toBeLessThanOrEqual(5);
+84 -7
View File
@@ -20,6 +20,35 @@ import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
import { SCAN_STATES } from "@/types/attack-paths";
const ORGANIZATION_SCAN_CONCURRENCY_LIMIT = 5;
interface OrganizationScanResource {
id: string;
type: string;
}
interface OrganizationScansSuccessResponse {
data: OrganizationScanResource[];
}
interface OrganizationScansErrorResponse {
error: unknown;
status?: number;
}
type OrganizationScansResponse =
| OrganizationScansSuccessResponse
| OrganizationScansErrorResponse;
const isOrganizationScanResource = (
value: unknown,
): value is OrganizationScanResource =>
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
"type" in value &&
value.type === "scans";
export const getScans = async ({
page = 1,
query = "",
@@ -183,9 +212,60 @@ export const scheduleDaily = async (formData: FormData) => {
};
export const launchOrganizationScans = async (
providerIds: string[],
scheduleOption: "daily" | "single",
) => {
organizationId: string,
): Promise<OrganizationScansResponse> => {
if (!organizationId) {
return { error: "Organization ID is required" };
}
const headers = await getAuthHeaders({ contentType: true });
const url = new URL(`${apiBaseUrl}/scans/bulk`);
try {
const response = await fetch(url.toString(), {
method: "POST",
headers,
body: JSON.stringify({
data: {
type: "scans-bulk",
relationships: {
organization: {
data: {
type: "organizations",
id: organizationId,
},
},
},
},
}),
});
const result = await handleApiResponse(response, "/scans");
if (result?.error !== undefined) {
return { error: result.error, status: result.status };
}
const scans: unknown = result?.data;
if (!Array.isArray(scans) || !scans.every(isOrganizationScanResource)) {
return {
error: "The bulk scan response did not contain a scan collection.",
};
}
addScanOperation("start", undefined, {
organization_id: organizationId,
bulk: true,
scan_count: scans.length,
scan_ids: scans.map((scan) => scan.id).join(","),
});
return { data: scans };
} catch (error) {
return handleApiError(error);
}
};
export const scheduleOrganizationDailyScans = async (providerIds: string[]) => {
const validProviderIds = providerIds.filter(Boolean);
if (validProviderIds.length === 0) {
return {
@@ -203,10 +283,7 @@ export const launchOrganizationScans = async (
const formData = new FormData();
formData.set("providerId", providerId);
const result =
scheduleOption === "daily"
? await scheduleDaily(formData)
: await scanOnDemand(formData);
const result = await scheduleDaily(formData);
return {
providerId,
@@ -52,6 +52,18 @@ describe("buildOverviewProviderContextItems", () => {
]);
});
it("dedupes repeated provider ids before filling the bounded slots", () => {
const items = buildOverviewProviderContextItems({
searchParams: {
"filter[provider_id__in]": "prov-1,prov-1,prov-1,prov-2,prov-3",
},
providers,
groups,
});
expect(items.map((item) => item.id)).toEqual(["prov-1", "prov-2"]);
});
it("resolves URL-filtered group ids to labeled group items", () => {
const items = buildOverviewProviderContextItems({
searchParams: { "filter[provider_groups__in]": "group-1,unknown" },
@@ -10,8 +10,8 @@ import type { ProviderProps } from "@/types/providers";
import { parseFilterIds } from "./provider-scope";
const OVERVIEW_PATHNAME = "/";
// Bounded so provider items cannot crowd out the page, ThreatScore, and
// posture summaries within the shared context item budget.
// Bounded so provider items take a small share of the context item budget;
// under byte pressure the compiler additionally evicts provider items first.
const MAX_PROVIDER_ITEMS = 2;
const MAX_TOTAL_ITEMS = 3;
@@ -26,8 +26,14 @@ export function buildOverviewProviderContextItems({
providers,
groups,
}: OverviewProviderContextInput): LighthouseProviderContextItem[] {
const providerIds = parseFilterIds(searchParams["filter[provider_id__in]"]);
const groupIds = parseFilterIds(searchParams["filter[provider_groups__in]"]);
// Dedupe before slicing so a repeated id cannot fill the bounded slots and
// silently push the remaining selected providers out of the context.
const providerIds = Array.from(
new Set(parseFilterIds(searchParams["filter[provider_id__in]"])),
);
const groupIds = Array.from(
new Set(parseFilterIds(searchParams["filter[provider_groups__in]"])),
);
const providerItems = providerIds
.map((id) => providers.find((provider) => provider.id === id))
@@ -1,6 +1,8 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { getFindingsByStatus } from "@/actions/overview";
import { CheckFindingsSSR } from "./check-findings.ssr";
vi.mock("@/actions/overview", () => ({
@@ -33,4 +35,34 @@ describe("CheckFindingsSSR", () => {
expect(context).toHaveTextContent('"newPassed":12');
expect(context).toHaveTextContent('"newFailed":7');
});
it("renders the error state and publishes no context on a 4xx response", async () => {
// handleApiResponse resolves truthy {error, status} objects for 4xx.
vi.mocked(getFindingsByStatus).mockResolvedValueOnce({
error: "Invalid filter",
status: 400,
});
render(await CheckFindingsSSR({ searchParams: {} }));
expect(
screen.getByText("Failed to load findings data"),
).toBeInTheDocument();
expect(screen.queryByTestId("status-context")).not.toBeInTheDocument();
});
it("renders the error state and publishes no context on an empty body", async () => {
// handleApiResponse resolves {success, status} for 204 and empty bodies.
vi.mocked(getFindingsByStatus).mockResolvedValueOnce({
success: true,
status: 204,
});
render(await CheckFindingsSSR({ searchParams: {} }));
expect(
screen.getByText("Failed to load findings data"),
).toBeInTheDocument();
expect(screen.queryByTestId("status-context")).not.toBeInTheDocument();
});
});
@@ -11,7 +11,9 @@ export const CheckFindingsSSR = async ({ searchParams }: SSRComponentProps) => {
const findingsByStatus = await getFindingsByStatus({ filters });
if (!findingsByStatus) {
// handleApiResponse resolves truthy on 4xx ({error, status}) and empty
// bodies ({success, status}), so only a payload with attributes is data.
if (!findingsByStatus?.data?.attributes) {
return (
<div className="flex h-[400px] w-full max-w-md items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
<p className="text-zinc-400">Failed to load findings data</p>
@@ -19,9 +21,12 @@ export const CheckFindingsSSR = async ({ searchParams }: SSRComponentProps) => {
);
}
const attributes = findingsByStatus?.data?.attributes || {};
const { fail = 0, pass = 0, fail_new = 0, pass_new = 0 } = attributes;
const {
fail = 0,
pass = 0,
fail_new = 0,
pass_new = 0,
} = findingsByStatus.data.attributes;
return (
<>
@@ -1,6 +1,8 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { getFindingsBySeverity } from "@/actions/overview";
import { RiskSeverityChartSSR } from "./risk-severity-chart.ssr";
vi.mock("@/actions/overview", () => ({
@@ -38,4 +40,34 @@ describe("RiskSeverityChartSSR", () => {
'"severityCounts":{"critical":4,"high":18,"medium":40,"low":15,"informational":3}',
);
});
it("renders the error state and publishes no context on a 4xx response", async () => {
// handleApiResponse resolves truthy {error, status} objects for 4xx.
vi.mocked(getFindingsBySeverity).mockResolvedValueOnce({
error: "Invalid filter",
status: 400,
} as unknown as Awaited<ReturnType<typeof getFindingsBySeverity>>);
render(await RiskSeverityChartSSR({ searchParams: {} }));
expect(
screen.getByText("Failed to load severity data"),
).toBeInTheDocument();
expect(screen.queryByTestId("severity-context")).not.toBeInTheDocument();
});
it("renders the error state and publishes no context on an empty body", async () => {
// handleApiResponse resolves {success, status} for 204 and empty bodies.
vi.mocked(getFindingsBySeverity).mockResolvedValueOnce({
success: true,
status: 204,
} as unknown as Awaited<ReturnType<typeof getFindingsBySeverity>>);
render(await RiskSeverityChartSSR({ searchParams: {} }));
expect(
screen.getByText("Failed to load severity data"),
).toBeInTheDocument();
expect(screen.queryByTestId("severity-context")).not.toBeInTheDocument();
});
});
@@ -16,7 +16,9 @@ export const RiskSeverityChartSSR = async ({
const findingsBySeverity = await getFindingsBySeverity({ filters });
if (!findingsBySeverity) {
// handleApiResponse resolves truthy on 4xx ({error, status}) and empty
// bodies ({success, status}), so only a payload with attributes is data.
if (!findingsBySeverity?.data?.attributes) {
return (
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
<p className="text-zinc-400">Failed to load severity data</p>
@@ -30,7 +32,7 @@ export const RiskSeverityChartSSR = async ({
medium = 0,
low = 0,
informational = 0,
} = findingsBySeverity?.data?.attributes || {};
} = findingsBySeverity.data.attributes;
return (
<>
@@ -0,0 +1,17 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ThreatScore } from "./threat-score";
describe("ThreatScore", () => {
it("keeps the card full width until the overview row becomes horizontal", () => {
render(<ThreatScore score={75} />);
const card = screen
.getByText("Prowler ThreatScore")
.closest('[data-slot="card"]');
expect(card).toHaveClass("xl:max-w-[312px]");
expect(card).not.toHaveClass("lg:max-w-[312px]");
});
});
@@ -115,7 +115,7 @@ export function ThreatScore({
return (
<Card
variant="base"
className="flex min-h-[372px] w-full flex-col justify-between lg:max-w-[312px]"
className="flex min-h-[372px] w-full flex-col justify-between xl:max-w-[312px]"
>
<CardHeader>
<CardTitle>Prowler ThreatScore</CardTitle>
@@ -54,7 +54,7 @@ describe("ThreatScoreSSR", () => {
},
},
],
});
} as unknown as Awaited<ReturnType<typeof getThreatScore>>);
render(await ThreatScoreSSR({ searchParams: {} }));
@@ -91,7 +91,7 @@ describe("ThreatScoreSSR", () => {
},
},
],
});
} as unknown as Awaited<ReturnType<typeof getThreatScore>>);
render(await ThreatScoreSSR({ searchParams: {} }));
@@ -104,4 +104,36 @@ describe("ThreatScoreSSR", () => {
'"totals":{"passed":120,"failed":40,"total":160}',
);
});
it("renders the empty state and publishes no context on a 4xx response", async () => {
// handleApiResponse resolves truthy {error, status} objects for 4xx.
vi.mocked(getThreatScore).mockResolvedValueOnce({
error: "Invalid filter",
status: 400,
} as unknown as Awaited<ReturnType<typeof getThreatScore>>);
render(await ThreatScoreSSR({ searchParams: {} }));
expect(screen.queryByTestId("overview-context")).not.toBeInTheDocument();
});
it("publishes a zero critical count when the field is absent", async () => {
vi.mocked(getThreatScore).mockResolvedValueOnce({
data: [
{
attributes: {
overall_score: "70",
score_delta: null,
section_scores: {},
},
},
],
} as unknown as Awaited<ReturnType<typeof getThreatScore>>);
render(await ThreatScoreSSR({ searchParams: {} }));
expect(screen.getByTestId("overview-context")).toHaveTextContent(
'"criticalRequirementsCount":0',
);
});
});
@@ -43,7 +43,8 @@ export const ThreatScoreSSR = async ({ searchParams }: SSRComponentProps) => {
framework: "Prowler ThreatScore",
score,
scoreDelta: scoreDelta ?? undefined,
criticalRequirementsCount: attributes.critical_requirements.length,
criticalRequirementsCount:
attributes.critical_requirements?.length ?? 0,
worstSection: worstSectionEntry?.[0],
worstSectionScore: worstSectionEntry?.[1],
passed: attributes.passed_requirements,
@@ -58,4 +58,16 @@ describe("ServiceWatchlistSSR", () => {
expect(screen.queryByTestId("service-context")).not.toBeInTheDocument();
});
it("publishes no service context on a 4xx response", async () => {
// handleApiResponse resolves truthy {error, status} objects for 4xx.
vi.mocked(getServicesOverview).mockResolvedValueOnce({
error: "Invalid filter",
status: 400,
} as unknown as Awaited<ReturnType<typeof getServicesOverview>>);
render(await ServiceWatchlistSSR({ searchParams: {} }));
expect(screen.queryByTestId("service-context")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,237 @@
import { render, screen } from "@testing-library/react";
import {
Children,
isValidElement,
Suspense,
type ReactElement,
type ReactNode,
} from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ComplianceDetail from "./page";
const {
getComplianceAttributesMock,
getComplianceOverviewMetadataInfoMock,
getComplianceRequirementsMock,
getScanMock,
mapComplianceDataMock,
} = vi.hoisted(() => ({
getComplianceAttributesMock: vi.fn(),
getComplianceOverviewMetadataInfoMock: vi.fn(),
getComplianceRequirementsMock: vi.fn(),
getScanMock: vi.fn(),
mapComplianceDataMock: vi.fn(),
}));
vi.mock("next/navigation", () => ({
notFound: vi.fn(() => {
throw new Error("notFound");
}),
redirect: vi.fn(() => {
throw new Error("redirect");
}),
}));
vi.mock("@/actions/compliances", () => ({
COMPLIANCE_OVERVIEW_RESOURCE_TYPE: { TASK: "tasks" },
getComplianceAttributes: getComplianceAttributesMock,
getComplianceOverviewMetadataInfo: getComplianceOverviewMetadataInfoMock,
getComplianceRequirements: getComplianceRequirementsMock,
getCompliancesOverview: vi.fn(),
}));
vi.mock("@/actions/overview", () => ({
getThreatScore: vi.fn(),
}));
vi.mock("@/actions/scans", () => ({
getScan: getScanMock,
}));
vi.mock("@/components/compliance", () => ({
ClientAccordionWrapper: () => <div>Empty requirements</div>,
ComplianceDownloadContainer: () => null,
ComplianceHeader: () => null,
ComplianceWarming: () => null,
RequirementsStatusCard: ({
pass,
fail,
manual,
}: {
pass: number;
fail: number;
manual: number;
}) => (
<div>
Requirements: {pass} pass, {fail} fail, {manual} manual
</div>
),
RequirementsStatusCardSkeleton: () => null,
SkeletonAccordion: () => null,
ThreatScoreBreakdownCard: () => null,
ThreatScoreBreakdownCardSkeleton: () => null,
TopFailedSectionsCard: () => null,
TopFailedSectionsCardSkeleton: () => null,
}));
vi.mock("@/components/icons/compliance/IconCompliance", () => ({
getComplianceIcon: vi.fn(),
}));
vi.mock("@/components/lighthouse/context-contributor", () => ({
LighthouseContextContributor: () => null,
}));
vi.mock("@/components/shadcn/button/button", () => ({
Button: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/shadcn/card/card", () => ({
Card: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/shadcn/content-layout", () => ({
ContentLayout: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("@/lib/compliance/compliance-mapper", () => ({
getComplianceMapper: () => ({
getTopFailedSections: vi.fn(() => []),
mapComplianceData: mapComplianceDataMock,
toAccordionItems: vi.fn(() => []),
}),
}));
vi.mock("@/lib/compliance/compliance-report-types", () => ({
getReportTypeForCompliance: vi.fn(),
pickLatestCisPerProvider: vi.fn(() => new Set()),
}));
vi.mock("@/lib/shared/env", () => ({
isCloud: () => false,
}));
vi.mock("../_components/cross-account-detail", () => ({
CrossAccountDetail: () => null,
}));
vi.mock("../_components/cross-provider-detail", () => ({
CrossProviderDetail: () => null,
}));
vi.mock("../_lib/cross-provider-frameworks", () => ({
resolveCrossProviderFramework: vi.fn(),
}));
vi.mock("../_lib/search-params-key", () => ({
buildSearchParamsKey: vi.fn(() => "search-params"),
}));
interface ContentLayoutTestProps {
children: ReactNode;
}
type AsyncServerComponent = (
props: Record<string, unknown>,
) => Promise<ReactNode>;
const renderPerScanContent = async () => {
const page = (await ComplianceDetail({
params: Promise.resolve({ compliancetitle: "ISO 27001" }),
searchParams: Promise.resolve({
complianceId: "iso27001_2022_aws",
scanId: "scan-1",
}),
})) as ReactElement<ContentLayoutTestProps>;
const suspense = Children.toArray(page.props.children).find(
(child) => isValidElement(child) && child.type === Suspense,
);
if (!isValidElement<{ children: ReactElement }>(suspense)) {
throw new Error("Expected the per-scan compliance Suspense boundary");
}
const content = suspense.props.children as ReactElement<
Record<string, unknown>,
AsyncServerComponent
>;
render(await content.type(content.props));
};
describe("Compliance detail task response", () => {
beforeEach(() => {
vi.clearAllMocks();
getComplianceOverviewMetadataInfoMock.mockResolvedValue({
data: { attributes: { regions: [] } },
});
getComplianceAttributesMock.mockResolvedValue({
data: [
{
id: "iso27001_2022_aws",
type: "compliance-overview-attributes",
attributes: {
compliance_name: "ISO 27001",
framework: "ISO27001",
},
},
],
});
getScanMock.mockResolvedValue(undefined);
mapComplianceDataMock.mockImplementation(
(_attributesData, requirementsData) => {
const requirements = requirementsData.data;
requirements.forEach(() => undefined);
return [];
},
);
});
it("renders an empty detail while requirements are being generated", async () => {
// Given - the requirements endpoint returned a JSON:API task resource
getComplianceRequirementsMock.mockResolvedValue({
data: {
id: "task-1",
type: "tasks",
attributes: { state: "executing" },
},
});
// When - the server-rendered detail handles the pending response
await renderPerScanContent();
// Then - the task never reaches the requirements array mapper
expect(
screen.getByText("Requirements: 0 pass, 0 fail, 0 manual"),
).toBeInTheDocument();
});
it("maps a completed requirements collection", async () => {
// Given - the requirements endpoint returned its normal collection
getComplianceRequirementsMock.mockResolvedValue({
data: [
{
id: "requirement-1",
type: "compliance-overview-requirements",
attributes: { status: "PASS" },
},
],
});
mapComplianceDataMock.mockReturnValue([
{
name: "ISO 27001",
pass: 1,
fail: 0,
manual: 0,
},
]);
// When - the server-rendered detail handles the completed response
await renderPerScanContent();
// Then - normal mapper output is still rendered
expect(
screen.getByText("Requirements: 1 pass, 0 fail, 0 manual"),
).toBeInTheDocument();
});
});
@@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation";
import { Suspense } from "react";
import {
COMPLIANCE_OVERVIEW_RESOURCE_TYPE,
getComplianceAttributes,
getComplianceOverviewMetadataInfo,
getComplianceRequirements,
@@ -267,7 +268,13 @@ export default async function ComplianceDetail({
const snapshot = threatScoreResponse.data[0];
threatScoreData = {
overallScore: parseFloat(snapshot.attributes.overall_score),
sectionScores: snapshot.attributes.section_scores,
// The multi-provider aggregation branch serializes section scores as
// decimal strings.
sectionScores: Object.fromEntries(
Object.entries(snapshot.attributes.section_scores).map(
([name, value]) => [name, Number(value)],
),
),
};
}
}
@@ -391,9 +398,14 @@ const SSRComplianceContent = async ({
scanId,
region,
});
const type = requirementsData?.data?.[0]?.type;
const requirements = requirementsData?.data;
const type = Array.isArray(requirements) ? undefined : requirements?.type;
if (!scanId || type === "tasks") {
if (
!scanId ||
type === COMPLIANCE_OVERVIEW_RESOURCE_TYPE.TASK ||
!Array.isArray(requirements)
) {
return (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]">
@@ -410,7 +422,7 @@ const SSRComplianceContent = async ({
const mapper = getComplianceMapper(framework);
const data = mapper.mapComplianceData(
attributesData,
requirementsData,
{ data: requirements },
filter,
);
// const categoryHeatmapData = mapper.calculateCategoryHeatmapData(data);

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