mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 17:40:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c076df74f9 | ||
|
|
5e812c614b | ||
|
|
cd3d5630b7 | ||
|
|
e3adb38bb6 | ||
|
|
1d3cefa490 | ||
|
|
5f94be2611 | ||
|
|
065ce48c0f | ||
|
|
8ee4ae2f46 | ||
|
|
2d535b19cc | ||
|
|
d480679638 | ||
|
|
6e24a49128 | ||
|
|
a9a60c9448 | ||
|
|
a180d8a775 | ||
|
|
43ecebc849 | ||
|
|
a70a69a7d0 | ||
|
|
6d7b1ef153 | ||
|
|
f26c814e5a | ||
|
|
587f9bcfa6 | ||
|
|
b12aa854d6 | ||
|
|
407ce0d4ca | ||
|
|
fbbf9fe1cd | ||
|
|
ca9feed805 | ||
|
|
234e69da3f | ||
|
|
f1b3b76efc | ||
|
|
5cd673787b |
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
@@ -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 }}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
Generated
+71
-60
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+20
-6
@@ -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 = (
|
||||
|
||||
+26
-1
@@ -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}"
|
||||
)
|
||||
|
||||
+26
-4
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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())
|
||||
|
||||
+8
-3
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+148
-26
@@ -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
|
||||
}
|
||||
|
||||
+242
-69
@@ -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
|
||||
|
||||
+68
-22
@@ -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)
|
||||
|
||||
+120
-111
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user