From 635e451d9b09d01c73220d183778d4fa150509c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:07:46 +0200 Subject: [PATCH 1/4] fix(container): verify the checksum of downloaded third-party binaries (#12334) --- Dockerfile | 17 +++++++++++++++++ api/Dockerfile | 17 +++++++++++++++++ .../api-verify-downloaded-binaries.security.md | 1 + .../verify-downloaded-binaries.security.md | 1 + 4 files changed, 36 insertions(+) create mode 100644 api/changelog.d/api-verify-downloaded-binaries.security.md create mode 100644 prowler/changelog.d/verify-downloaded-binaries.security.md diff --git a/Dockerfile b/Dockerfile index 6a7318280b..62aa3c4f1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,14 @@ ENV TRIVY_VERSION=${TRIVY_VERSION} ARG ZIZMOR_VERSION=1.24.1 ENV ZIZMOR_VERSION=${ZIZMOR_VERSION} +# Pinned here, not fetched with the artefact: a compromised release ships its own checksum. +ARG TRIVY_SHA256_AMD64=bbb64b9695866ce4a7a8f5c9592002c5961cab378577fa3f8a040df362b9b2ea +ARG TRIVY_SHA256_ARM64=2ca2c023109c2db6b2b77366b6717291452d4531167377d95c79547f0c8e3467 +ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0 +ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56 +ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03 +ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d + # hadolint ignore=DL3008 RUN apt-get update && apt-get install -y --no-install-recommends \ wget libicu76 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \ @@ -29,6 +37,9 @@ RUN ARCH=$(uname -m) && \ else \ echo "Unsupported architecture: $ARCH" && exit 1 ; \ fi && \ + if [ "$ARCH" = "x86_64" ]; then EXPECT="$POWERSHELL_SHA256_AMD64" ; else EXPECT="$POWERSHELL_SHA256_ARM64" ; fi && \ + echo "$EXPECT /tmp/powershell.tar.gz" > /tmp/powershell.sha256 && \ + sha256sum -c /tmp/powershell.sha256 && rm /tmp/powershell.sha256 && \ mkdir -p /opt/microsoft/powershell/7 && \ tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 && \ chmod +x /opt/microsoft/powershell/7/pwsh && \ @@ -45,6 +56,9 @@ RUN ARCH=$(uname -m) && \ echo "Unsupported architecture for Trivy: $ARCH" && exit 1 ; \ fi && \ wget --progress=dot:giga "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_${TRIVY_ARCH}.tar.gz" -O /tmp/trivy.tar.gz && \ + if [ "$ARCH" = "x86_64" ]; then EXPECT="$TRIVY_SHA256_AMD64" ; else EXPECT="$TRIVY_SHA256_ARM64" ; fi && \ + echo "$EXPECT /tmp/trivy.tar.gz" > /tmp/trivy.sha256 && \ + sha256sum -c /tmp/trivy.sha256 && rm /tmp/trivy.sha256 && \ tar zxf /tmp/trivy.tar.gz -C /tmp && \ mv /tmp/trivy /usr/local/bin/trivy && \ chmod +x /usr/local/bin/trivy && \ @@ -63,6 +77,9 @@ RUN ARCH=$(uname -m) && \ echo "Unsupported architecture for zizmor: $ARCH" && exit 1 ; \ fi && \ wget --progress=dot:giga "https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-${ZIZMOR_ARCH}.tar.gz" -O /tmp/zizmor.tar.gz && \ + if [ "$ARCH" = "x86_64" ]; then EXPECT="$ZIZMOR_SHA256_AMD64" ; else EXPECT="$ZIZMOR_SHA256_ARM64" ; fi && \ + echo "$EXPECT /tmp/zizmor.tar.gz" > /tmp/zizmor.sha256 && \ + sha256sum -c /tmp/zizmor.sha256 && rm /tmp/zizmor.sha256 && \ mkdir -p /tmp/zizmor-extract && \ tar zxf /tmp/zizmor.tar.gz -C /tmp/zizmor-extract && \ mv /tmp/zizmor-extract/zizmor /usr/local/bin/zizmor && \ diff --git a/api/Dockerfile b/api/Dockerfile index c6992de25d..af7b07e16c 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -13,6 +13,14 @@ ENV TRIVY_VERSION=${TRIVY_VERSION} ARG ZIZMOR_VERSION=1.24.1 ENV ZIZMOR_VERSION=${ZIZMOR_VERSION} +# Pinned here, not fetched with the artefact: a compromised release ships its own checksum. +ARG TRIVY_SHA256_AMD64=bbb64b9695866ce4a7a8f5c9592002c5961cab378577fa3f8a040df362b9b2ea +ARG TRIVY_SHA256_ARM64=2ca2c023109c2db6b2b77366b6717291452d4531167377d95c79547f0c8e3467 +ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0 +ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56 +ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03 +ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d + # hadolint ignore=DL3008 RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ @@ -39,6 +47,9 @@ RUN ARCH=$(uname -m) && \ else \ echo "Unsupported architecture: $ARCH" && exit 1 ; \ fi && \ + if [ "$ARCH" = "x86_64" ]; then EXPECT="$POWERSHELL_SHA256_AMD64" ; else EXPECT="$POWERSHELL_SHA256_ARM64" ; fi && \ + echo "$EXPECT /tmp/powershell.tar.gz" > /tmp/powershell.sha256 && \ + sha256sum -c /tmp/powershell.sha256 && rm /tmp/powershell.sha256 && \ mkdir -p /opt/microsoft/powershell/7 && \ tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 && \ chmod +x /opt/microsoft/powershell/7/pwsh && \ @@ -55,6 +66,9 @@ RUN ARCH=$(uname -m) && \ echo "Unsupported architecture for Trivy: $ARCH" && exit 1 ; \ fi && \ wget --progress=dot:giga "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_${TRIVY_ARCH}.tar.gz" -O /tmp/trivy.tar.gz && \ + if [ "$ARCH" = "x86_64" ]; then EXPECT="$TRIVY_SHA256_AMD64" ; else EXPECT="$TRIVY_SHA256_ARM64" ; fi && \ + echo "$EXPECT /tmp/trivy.tar.gz" > /tmp/trivy.sha256 && \ + sha256sum -c /tmp/trivy.sha256 && rm /tmp/trivy.sha256 && \ tar zxf /tmp/trivy.tar.gz -C /tmp && \ mv /tmp/trivy /usr/local/bin/trivy && \ chmod +x /usr/local/bin/trivy && \ @@ -73,6 +87,9 @@ RUN ARCH=$(uname -m) && \ echo "Unsupported architecture for zizmor: $ARCH" && exit 1 ; \ fi && \ wget --progress=dot:giga "https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-${ZIZMOR_ARCH}.tar.gz" -O /tmp/zizmor.tar.gz && \ + if [ "$ARCH" = "x86_64" ]; then EXPECT="$ZIZMOR_SHA256_AMD64" ; else EXPECT="$ZIZMOR_SHA256_ARM64" ; fi && \ + echo "$EXPECT /tmp/zizmor.tar.gz" > /tmp/zizmor.sha256 && \ + sha256sum -c /tmp/zizmor.sha256 && rm /tmp/zizmor.sha256 && \ mkdir -p /tmp/zizmor-extract && \ tar zxf /tmp/zizmor.tar.gz -C /tmp/zizmor-extract && \ mv /tmp/zizmor-extract/zizmor /usr/local/bin/zizmor && \ diff --git a/api/changelog.d/api-verify-downloaded-binaries.security.md b/api/changelog.d/api-verify-downloaded-binaries.security.md new file mode 100644 index 0000000000..1b4e1d3b1d --- /dev/null +++ b/api/changelog.d/api-verify-downloaded-binaries.security.md @@ -0,0 +1 @@ +The API container image now verifies the checksum of every third-party binary it downloads (PowerShell, Trivy, zizmor) before installing it diff --git a/prowler/changelog.d/verify-downloaded-binaries.security.md b/prowler/changelog.d/verify-downloaded-binaries.security.md new file mode 100644 index 0000000000..609f754f29 --- /dev/null +++ b/prowler/changelog.d/verify-downloaded-binaries.security.md @@ -0,0 +1 @@ +The SDK container image now verifies the checksum of every third-party binary it downloads (PowerShell, Trivy, zizmor) before installing it From 87bc1eceae6213e195a38b9337a03454f9e7e742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:49:53 +0200 Subject: [PATCH 2/4] ci(container): scan images with Grype alongside Trivy, blocking on critical and high (#12340) --- .github/actions/grype-scan/action.yml | 128 +++++++++++ .github/workflows/api-container-checks.yml | 13 ++ .github/workflows/mcp-container-checks.yml | 12 + .github/workflows/sdk-container-checks.yml | 13 ++ .github/workflows/ui-container-checks.yml | 12 + .grype.yaml | 211 ++++++++++++++++++ .../api-aiohttp-cve-2026-69244.security.md | 1 + api/pyproject.toml | 2 +- api/uv.lock | 78 +++---- .../aiohttp-cve-2026-69244.security.md | 1 + pyproject.toml | 2 +- uv.lock | 162 +++++++------- 12 files changed, 513 insertions(+), 122 deletions(-) create mode 100644 .github/actions/grype-scan/action.yml create mode 100644 .grype.yaml create mode 100644 api/changelog.d/api-aiohttp-cve-2026-69244.security.md create mode 100644 prowler/changelog.d/aiohttp-cve-2026-69244.security.md diff --git a/.github/actions/grype-scan/action.yml b/.github/actions/grype-scan/action.yml new file mode 100644 index 0000000000..1cac3f7674 --- /dev/null +++ b/.github/actions/grype-scan/action.yml @@ -0,0 +1,128 @@ +name: 'Container Security Scan with Grype' +description: 'Scans container images for vulnerabilities using Grype and reports results' +author: 'Prowler' + +inputs: + image-name: + description: 'Container image name to scan' + required: true + image-tag: + description: 'Container image tag to scan' + required: true + default: ${{ github.sha }} + fail-on-severity: + description: 'Fail the build on findings at this severity or above: critical, high, or none' + required: false + default: 'high' + upload-sarif: + description: 'Upload results to GitHub Security tab' + required: false + default: 'true' + artifact-retention-days: + description: 'Days to retain the Grype report artifact' + required: false + default: '2' + +outputs: + critical-count: + description: 'Number of critical vulnerabilities found' + value: ${{ steps.security-check.outputs.critical }} + high-count: + description: 'Number of high vulnerabilities found' + value: ${{ steps.security-check.outputs.high }} + total-count: + description: 'Total number of vulnerabilities found' + value: ${{ steps.security-check.outputs.total }} + +runs: + using: 'composite' + steps: + - name: Run Grype vulnerability scan (JSON) + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + with: + image: ${{ inputs.image-name }}:${{ inputs.image-tag }} + output-format: 'json' + output-file: 'grype-report.json' + fail-build: 'false' + by-cve: 'true' # Report CVE ids rather than GHSA, so findings line up with Trivy's + cache-db: 'true' + grype-version: 'v0.116.1' + + - name: Run Grype vulnerability scan (SARIF) + if: inputs.upload-sarif == 'true' && github.event_name == 'push' + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + with: + image: ${{ inputs.image-name }}:${{ inputs.image-tag }} + output-format: 'sarif' + output-file: 'grype-results.sarif' + fail-build: 'false' + severity-cutoff: 'high' + by-cve: 'true' + cache-db: 'true' + grype-version: 'v0.116.1' + + - name: Upload Grype results to GitHub Security tab + if: inputs.upload-sarif == 'true' && github.event_name == 'push' + uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + with: + sarif_file: 'grype-results.sarif' + category: 'grype-container' + + - name: Upload Grype report artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + with: + name: grype-scan-report-${{ inputs.image-name }}-${{ inputs.image-tag }} + path: grype-report.json + retention-days: ${{ inputs.artifact-retention-days }} + + - name: Generate security summary + id: security-check + shell: bash + run: | + CRITICAL=$(jq '[.matches[]? | select(.vulnerability.severity=="Critical")] | length' grype-report.json) + HIGH=$(jq '[.matches[]? | select(.vulnerability.severity=="High")] | length' grype-report.json) + TOTAL=$(jq '[.matches[]?] | length' grype-report.json) + + echo "critical=$CRITICAL" >> $GITHUB_OUTPUT + echo "high=$HIGH" >> $GITHUB_OUTPUT + echo "total=$TOTAL" >> $GITHUB_OUTPUT + + echo "### 🔎 Container Security Scan (Grype)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${INPUTS_IMAGE_NAME}:${INPUTS_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 🔴 Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY + echo "- 🟠 High: $HIGH" >> $GITHUB_STEP_SUMMARY + echo "- **Total**: $TOTAL" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Reported alongside Trivy, not instead of it. Counts differ by design." >> $GITHUB_STEP_SUMMARY + env: + INPUTS_IMAGE_NAME: ${{ inputs.image-name }} + INPUTS_IMAGE_TAG: ${{ inputs.image-tag }} + + - name: Check for blocking vulnerabilities + if: inputs.fail-on-severity != 'none' + shell: bash + run: | + if [ "$CUTOFF" = "critical" ]; then + BLOCKING=$CRITICAL + SEVERITIES='["Critical"]' + else + BLOCKING=$((CRITICAL + HIGH)) + SEVERITIES='["Critical","High"]' + fi + + if [ "$BLOCKING" -gt 0 ]; then + echo "::error::Found $BLOCKING vulnerabilities at severity ${CUTOFF} or above ($CRITICAL critical, $HIGH high)" + echo "::warning::Update the package, or add it to .grype.yaml with a reason if nothing can be done" + jq -r --argjson severities "$SEVERITIES" \ + '.matches[] | select(.vulnerability.severity | IN($severities[])) + | " \(.vulnerability.severity)\t\(.vulnerability.id)\t\(.artifact.name) \(.artifact.version)"' \ + grype-report.json | sort -u + exit 1 + fi + env: + CUTOFF: ${{ inputs.fail-on-severity }} + CRITICAL: ${{ steps.security-check.outputs.critical }} + HIGH: ${{ steps.security-check.outputs.high }} diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml index c540b6e533..19e6de3a0f 100644 --- a/.github/workflows/api-container-checks.yml +++ b/.github/workflows/api-container-checks.yml @@ -81,6 +81,9 @@ jobs: auth.docker.io:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 + raw.githubusercontent.com:443 + objects.githubusercontent.com:443 + grype.anchore.io:443 debian.map.fastlydns.net:80 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 @@ -108,6 +111,8 @@ jobs: files: | api/** .github/actions/trivy-scan/** + .github/actions/grype-scan/** + .grype.yaml files_ignore: | api/docs/** api/README.md @@ -147,3 +152,11 @@ jobs: image-tag: ${{ github.sha }} fail-on-critical: 'true' severity: 'CRITICAL' + + - name: Scan container with Grype + if: steps.check-changes.outputs.any_changed == 'true' + uses: ./.github/actions/grype-scan + with: + image-name: ${{ env.IMAGE_NAME }} + image-tag: ${{ github.sha }} + fail-on-severity: 'high' diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml index c8a1830501..a9c19f716d 100644 --- a/.github/workflows/mcp-container-checks.yml +++ b/.github/workflows/mcp-container-checks.yml @@ -87,6 +87,8 @@ jobs: get.trivy.dev:443 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 + raw.githubusercontent.com:443 + grype.anchore.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -101,6 +103,8 @@ jobs: files: | mcp_server/** .github/actions/trivy-scan/** + .github/actions/grype-scan/** + .grype.yaml files_ignore: | mcp_server/README.md mcp_server/CHANGELOG.md @@ -129,3 +133,11 @@ jobs: image-tag: ${{ github.sha }} fail-on-critical: 'true' severity: 'CRITICAL' + + - name: Scan MCP container with Grype + if: steps.check-changes.outputs.any_changed == 'true' + uses: ./.github/actions/grype-scan + with: + image-name: ${{ env.IMAGE_NAME }} + image-tag: ${{ github.sha }} + fail-on-severity: 'high' diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml index aba7ee98bf..b98207b978 100644 --- a/.github/workflows/sdk-container-checks.yml +++ b/.github/workflows/sdk-container-checks.yml @@ -83,6 +83,9 @@ jobs: api.github.com:443 mirror.gcr.io:443 check.trivy.dev:443 + raw.githubusercontent.com:443 + objects.githubusercontent.com:443 + grype.anchore.io:443 debian.map.fastlydns.net:80 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 @@ -114,6 +117,8 @@ jobs: uv.lock .github/workflows/sdk-container-checks.yml .github/actions/trivy-scan/** + .github/actions/grype-scan/** + .grype.yaml files_ignore: | prowler/CHANGELOG.md prowler/changelog.d/** @@ -142,3 +147,11 @@ jobs: image-tag: ${{ github.sha }} fail-on-critical: 'true' severity: 'CRITICAL' + + - name: Scan SDK container with Grype + if: steps.check-changes.outputs.any_changed == 'true' + uses: ./.github/actions/grype-scan + with: + image-name: ${{ env.IMAGE_NAME }} + image-tag: ${{ github.sha }} + fail-on-severity: 'high' diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml index 05a3a8e3e0..5da65da3f1 100644 --- a/.github/workflows/ui-container-checks.yml +++ b/.github/workflows/ui-container-checks.yml @@ -88,6 +88,8 @@ jobs: get.trivy.dev:443 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 + raw.githubusercontent.com:443 + grype.anchore.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -102,6 +104,8 @@ jobs: files: | ui/** .github/actions/trivy-scan/** + .github/actions/grype-scan/** + .grype.yaml files_ignore: | ui/CHANGELOG.md ui/changelog.d/** @@ -134,3 +138,11 @@ jobs: image-tag: ${{ github.sha }} fail-on-critical: 'true' severity: 'CRITICAL' + + - name: Scan UI container with Grype + if: steps.check-changes.outputs.any_changed == 'true' + uses: ./.github/actions/grype-scan + with: + image-name: ${{ env.IMAGE_NAME }} + image-tag: ${{ github.sha }} + fail-on-severity: 'high' diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 0000000000..970fda5319 --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,211 @@ +# 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. + +ignore: + + # Operating-system packages with no fix available. + # Debian and Alpine have either published no fix or marked these will-not-fix. + # perl-base additionally cannot be removed: the distribution marks it Essential. + - vulnerability: CVE-2026-41992 + package: + name: gzip + - vulnerability: CVE-2026-54369 + package: + name: libacl1 + - vulnerability: CVE-2026-54370 + package: + name: libacl1 + - vulnerability: CVE-2026-5435 + package: + name: libc-bin + - vulnerability: CVE-2026-5450 + package: + name: libc-bin + - vulnerability: CVE-2026-5928 + package: + name: libc-bin + - vulnerability: CVE-2026-5435 + package: + name: libc6 + - vulnerability: CVE-2026-5450 + package: + name: libc6 + - vulnerability: CVE-2026-5928 + package: + name: libc6 + - vulnerability: CVE-2026-10536 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-11856 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-12064 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-8286 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-8924 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-8926 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-8927 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-8932 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-9079 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-9080 + package: + name: libcurl4t64 + - vulnerability: CVE-2026-9545 + package: + name: libcurl4t64 + - vulnerability: CVE-2025-69720 + package: + name: libncursesw6 + - vulnerability: CVE-2026-11822 + package: + name: libsqlite3-0 + - vulnerability: CVE-2026-11824 + package: + name: libsqlite3-0 + - vulnerability: CVE-2026-66032 + package: + name: libssh2-1t64 + - vulnerability: CVE-2026-66033 + package: + name: libssh2-1t64 + - vulnerability: CVE-2026-66034 + package: + name: libssh2-1t64 + - vulnerability: CVE-2026-66035 + package: + name: libssh2-1t64 + - vulnerability: CVE-2025-69720 + package: + name: libtinfo6 + - vulnerability: CVE-2025-69720 + package: + name: ncurses-base + - vulnerability: CVE-2025-69720 + package: + name: ncurses-bin + - vulnerability: CVE-2026-12087 + package: + name: perl-base + - vulnerability: CVE-2026-13221 + package: + name: perl-base + - vulnerability: CVE-2026-42496 + package: + name: perl-base + - vulnerability: CVE-2026-42497 + package: + name: perl-base + - vulnerability: CVE-2026-48959 + package: + name: perl-base + - vulnerability: CVE-2026-48961 + package: + name: perl-base + - vulnerability: CVE-2026-48962 + package: + name: perl-base + - vulnerability: CVE-2026-57432 + package: + name: perl-base + - vulnerability: CVE-2026-57433 + package: + name: perl-base + - vulnerability: CVE-2026-7017 + package: + name: perl-base + - vulnerability: CVE-2026-8376 + package: + name: perl-base + - vulnerability: CVE-2026-9538 + package: + name: perl-base + - vulnerability: CVE-2026-11822 + package: + name: sqlite-libs + - vulnerability: CVE-2026-11824 + package: + name: sqlite-libs + - vulnerability: CVE-2026-58469 + package: + name: wget + - vulnerability: CVE-2026-58471 + package: + name: wget + - vulnerability: CVE-2026-58472 + package: + name: wget + + # 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 + + # cryptography is capped below 49 by alibabacloud-tea-openapi==0.4.5, + # which requires cryptography>=3.0.0,<49.0.0. The fix is in 50.0.0, so it is + # unreachable until that dependency raises its ceiling. + - vulnerability: CVE-2026-69247 + package: + name: cryptography + + # The CPython interpreter, compiled into the official base image. + # TEMPORARY, unlike the entries above: these are reachable by moving to Python 3.13, + # which is a runtime upgrade pending its own evaluation. Three of them (CVE-2026-11940, + # CVE-2026-11972, CVE-2026-15308) need 3.15 and remain 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 diff --git a/api/changelog.d/api-aiohttp-cve-2026-69244.security.md b/api/changelog.d/api-aiohttp-cve-2026-69244.security.md new file mode 100644 index 0000000000..58de7b0493 --- /dev/null +++ b/api/changelog.d/api-aiohttp-cve-2026-69244.security.md @@ -0,0 +1 @@ +Upgrade aiohttp to 3.14.3 to pick up the fix for CVE-2026-69244 diff --git a/api/pyproject.toml b/api/pyproject.toml index 341bb0be57..547bd3f08b 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -101,7 +101,7 @@ constraint-dependencies = [ "aiobotocore==2.25.1", "aiofiles==24.1.0", "aiohappyeyeballs==2.6.1", - "aiohttp==3.14.0", + "aiohttp==3.14.3", "aioitertools==0.13.0", "aiosignal==1.4.0", "alibabacloud-actiontrail20200706==2.4.1", diff --git a/api/uv.lock b/api/uv.lock index 48756f6038..4b78b93a69 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -16,7 +16,7 @@ constraints = [ { name = "aiobotocore", specifier = "==2.25.1" }, { name = "aiofiles", specifier = "==24.1.0" }, { name = "aiohappyeyeballs", specifier = "==2.6.1" }, - { name = "aiohttp", specifier = "==3.14.0" }, + { name = "aiohttp", specifier = "==3.14.3" }, { name = "aioitertools", specifier = "==0.13.0" }, { name = "aiosignal", specifier = "==1.4.0" }, { name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" }, @@ -478,7 +478,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.0" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -490,44 +490,44 @@ dependencies = [ { name = "typing-extensions" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" }, - { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" }, - { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" }, - { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" }, - { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" }, - { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" }, - { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" }, - { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" }, - { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" }, - { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, ] [[package]] diff --git a/prowler/changelog.d/aiohttp-cve-2026-69244.security.md b/prowler/changelog.d/aiohttp-cve-2026-69244.security.md new file mode 100644 index 0000000000..58de7b0493 --- /dev/null +++ b/prowler/changelog.d/aiohttp-cve-2026-69244.security.md @@ -0,0 +1 @@ +Upgrade aiohttp to 3.14.3 to pick up the fix for CVE-2026-69244 diff --git a/pyproject.toml b/pyproject.toml index 08b66a31d2..383b5fb19c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,7 +174,7 @@ constraint-dependencies = [ "aenum==3.1.17", "aiofiles==24.1.0", "aiohappyeyeballs==2.6.1", - "aiohttp==3.14.0", + "aiohttp==3.14.3", "aiosignal==1.4.0", "alibabacloud-actiontrail20200706==2.4.1", "alibabacloud-credentials==1.0.3", diff --git a/uv.lock b/uv.lock index c16e98107b..7216ddc095 100644 --- a/uv.lock +++ b/uv.lock @@ -14,7 +14,7 @@ constraints = [ { name = "aenum", specifier = "==3.1.17" }, { name = "aiofiles", specifier = "==24.1.0" }, { name = "aiohappyeyeballs", specifier = "==2.6.1" }, - { name = "aiohttp", specifier = "==3.14.0" }, + { name = "aiohttp", specifier = "==3.14.3" }, { name = "aiosignal", specifier = "==1.4.0" }, { name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" }, { name = "alibabacloud-credentials", specifier = "==1.0.3" }, @@ -269,7 +269,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.0" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -282,85 +282,85 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/f0/f81190ba488cd106c2fc6d92680e56bb223bbbbf1e6908c2617011290112/aiohttp-3.14.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:692e409052e7436029bbb32977cd7c5bf806ac5fa4085b973996785ffadad33c", size = 760606, upload-time = "2026-06-01T19:36:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/f6/54/444d37eebf0f15db661ca44ec7caf93962f3c5ca92eb4c9a5d888b70aaa2/aiohttp-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40af7ebe53c7990e110dc4ad03566b12c3ac996254298a3d39046dd69cfcb2c2", size = 514677, upload-time = "2026-06-01T19:36:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d1/da280e23321c132c0a3fa7c8cc2830621d79174edc64c829443346489a36/aiohttp-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb2ffbb7da32f82e21ad9952669c45bd88a80e0878264c2f59fe1c6fb2badd", size = 510155, upload-time = "2026-06-01T19:36:44.072Z" }, - { url = "https://files.pythonhosted.org/packages/09/b8/2e36d54d0991ec5bba451444004591ee0af58cb1662a3a81c562878b9c1f/aiohttp-3.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2514cb7195f6d7c219339635bea71ae47d1569b051300d32df9dcfabcdb869", size = 1699947, upload-time = "2026-06-01T19:36:45.762Z" }, - { url = "https://files.pythonhosted.org/packages/57/95/a31d8ea1a0b9ecc084f5a7dd0b431ce64ef585918bb7bdc82afe11843877/aiohttp-3.14.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:30e8b7eeb42d02c120ca90d6c6e076a221a16b70a6dac9ae44c7ab5104cc7fe4", size = 1664364, upload-time = "2026-06-01T19:36:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/01/f6/5de3ddffc87a9e8d09b3be38fbd6dd1a736b2ad477a7e787dcb85f57f338/aiohttp-3.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63e38be0d75a654deaa06be32fb4cab883a4222940be1d05861b6717679cbadb", size = 1761186, upload-time = "2026-06-01T19:36:49.355Z" }, - { url = "https://files.pythonhosted.org/packages/33/8c/03c5438ec35d7e3a4f33fe895d6c3ec7540a7cec46065f21851211e1ee4d/aiohttp-3.14.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1210d4c87cc00128160c7384ab41877a701295b97cffa6362f908a49b6e8a7ca", size = 1849727, upload-time = "2026-06-01T19:36:51.478Z" }, - { url = "https://files.pythonhosted.org/packages/22/32/5a05303b0874458920b73f48b8779cc3a93d503f121b38dcc0456dbd698c/aiohttp-3.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a78a77366ed158a0a54b076990e575d7b7cdb728cbfd02711eadab150f2269f", size = 1708197, upload-time = "2026-06-01T19:36:53.241Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/478f169488d61414c0a05e7fe423b59ae3d9dcc933d1f0e4acc2c5d5bc3e/aiohttp-3.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f4d2038c64f36df96cfd3fa0937910e231eafbf897e70a06c155a817bb632fa6", size = 1578147, upload-time = "2026-06-01T19:36:55.154Z" }, - { url = "https://files.pythonhosted.org/packages/1d/af/b20af85765658972d3337834bd5eebba91b962794f2b4fc3e0ee8c85c0e1/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4714c70067a08b604d0bf3bc4dfdf82e52944afab41d0428d460862763d2f79b", size = 1665836, upload-time = "2026-06-01T19:36:56.94Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a3/771879cfd59948f4544b172189048905feff802f20f1c6c5411e998a3e06/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f79bfd2847513a7ac801bbafd1de02348a37926ac439eeb4bfe96fcff4eada15", size = 1680335, upload-time = "2026-06-01T19:36:58.642Z" }, - { url = "https://files.pythonhosted.org/packages/f4/16/582e36ad1d32133cd40659f3bc98e71c22179665a1cfbbb4713bce339c06/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:25e9f1d2465a210d60edb64d7b204a147e85d4c194eecef3d1604fb5ace678ce", size = 1731180, upload-time = "2026-06-01T19:37:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/11/bc/80708fe3f64a07a2c306a42fc7b009118a952709761d215f6d1b4c57195b/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b5314743ebe926c2fda35d0a298c565c885505f6635c2a30936363404cf274a7", size = 1565805, upload-time = "2026-06-01T19:37:02.446Z" }, - { url = "https://files.pythonhosted.org/packages/57/8f/8d25897f8273a32fe4ad40a8885eec4f397377ed46e8e383078169f60316/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:28eee8de1d69711c53116df8202f1c2aa0e3f80ef912a88fc18d159d53e7110b", size = 1742496, upload-time = "2026-06-01T19:37:04.222Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7d/c341d32ab2dec56c8478740695743dc6c21b383cace9376a3eab16311a07/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89ed35666c95d3efe1955056afcde09e62a57a34e2a4398b17f9f6c1564f0b25", size = 1691240, upload-time = "2026-06-01T19:37:06.277Z" }, - { url = "https://files.pythonhosted.org/packages/37/0f/a81207dd7a2d4a4f645b3a3f8b5a1da1159dc63117ffb137b698fd6df50f/aiohttp-3.14.0-cp310-cp310-win32.whl", hash = "sha256:5e4646e9a6af29af354204011bf5769cb0276ec5b64653e42f90b3e13845169f", size = 454686, upload-time = "2026-06-01T19:37:07.96Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/842357f2afb9c915715c6f5775239d987f5d0f845abf7675fa794e0a9d40/aiohttp-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:22a8d06f204e0518a586d770032db3c7043c9ba3693081b3e3ad425e1458d594", size = 478677, upload-time = "2026-06-01T19:37:09.652Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d1/330fb22c9535ec177b52396905131c6e39447244b6ca876262939af668ef/aiohttp-3.14.0-cp310-cp310-win_arm64.whl", hash = "sha256:4acfc34bd4d3c58754fc9f22ff1b5e92aabce68f3d4bf7b71a0b732d9bceb78a", size = 450364, upload-time = "2026-06-01T19:37:11.279Z" }, - { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" }, - { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" }, - { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" }, - { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" }, - { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" }, - { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" }, - { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" }, - { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" }, - { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" }, - { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" }, - { url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" }, - { url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" }, - { url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" }, - { url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" }, - { url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" }, - { url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" }, - { url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" }, - { url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" }, - { url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" }, - { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" }, - { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" }, + { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" }, + { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, ] [[package]] @@ -1987,7 +1987,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ From 8bf788ea957d4004888c440400253e6e1c611f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Wed, 5 Aug 2026 09:48:00 +0200 Subject: [PATCH 3/4] fix(aws): delegated administrator lookup and reporting (#12319) --- .../delegated-admin-unknown-manual.changed.md | 1 + .../guardduty-delegated-admin-lookup.fixed.md | 1 + ...curityhub-admin-lookup-per-region.fixed.md | 1 + ...ecurityhub-delegated-admin-fields.fixed.md | 1 + ...ed_admin_and_org_aggregator_all_regions.py | 26 +- ...uty_delegated_admin_enabled_all_regions.py | 27 +- .../services/guardduty/guardduty_service.py | 30 +- ...hub_delegated_admin_enabled_all_regions.py | 30 +- .../securityhub/securityhub_service.py | 33 +- ...min_and_org_aggregator_all_regions_test.py | 11 +- ...elegated_admin_enabled_all_regions_test.py | 174 ++++++++-- ...elegated_admin_enabled_all_regions_test.py | 311 ++++++++++++++---- .../securityhub_enabled_fixer_test.py | 90 +++-- .../securityhub_enabled_test.py | 231 ++++++------- .../securityhub/securityhub_service_test.py | 84 ++++- tests/providers/aws/utils.py | 38 +++ 16 files changed, 837 insertions(+), 252 deletions(-) create mode 100644 prowler/changelog.d/delegated-admin-unknown-manual.changed.md create mode 100644 prowler/changelog.d/guardduty-delegated-admin-lookup.fixed.md create mode 100644 prowler/changelog.d/securityhub-admin-lookup-per-region.fixed.md create mode 100644 prowler/changelog.d/securityhub-delegated-admin-fields.fixed.md diff --git a/prowler/changelog.d/delegated-admin-unknown-manual.changed.md b/prowler/changelog.d/delegated-admin-unknown-manual.changed.md new file mode 100644 index 0000000000..ecb3a1e15e --- /dev/null +++ b/prowler/changelog.d/delegated-admin-unknown-manual.changed.md @@ -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 diff --git a/prowler/changelog.d/guardduty-delegated-admin-lookup.fixed.md b/prowler/changelog.d/guardduty-delegated-admin-lookup.fixed.md new file mode 100644 index 0000000000..dc229a1a5d --- /dev/null +++ b/prowler/changelog.d/guardduty-delegated-admin-lookup.fixed.md @@ -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 diff --git a/prowler/changelog.d/securityhub-admin-lookup-per-region.fixed.md b/prowler/changelog.d/securityhub-admin-lookup-per-region.fixed.md new file mode 100644 index 0000000000..bccb6ebde8 --- /dev/null +++ b/prowler/changelog.d/securityhub-admin-lookup-per-region.fixed.md @@ -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 diff --git a/prowler/changelog.d/securityhub-delegated-admin-fields.fixed.md b/prowler/changelog.d/securityhub-delegated-admin-fields.fixed.md new file mode 100644 index 0000000000..958850bb27 --- /dev/null +++ b/prowler/changelog.d/securityhub-delegated-admin-fields.fixed.md @@ -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 diff --git a/prowler/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions.py b/prowler/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions.py index f56ea191f8..77e8b0fab8 100644 --- a/prowler/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions.py +++ b/prowler/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions.py @@ -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 = ( diff --git a/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py index 23065abd2b..c63abf5370 100644 --- a/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py +++ b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py @@ -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 = ( diff --git a/prowler/providers/aws/services/guardduty/guardduty_service.py b/prowler/providers/aws/services/guardduty/guardduty_service.py index bde8725454..ed575e6a4b 100644 --- a/prowler/providers/aws/services/guardduty/guardduty_service.py +++ b/prowler/providers/aws/services/guardduty/guardduty_service.py @@ -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}" ) diff --git a/prowler/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions.py b/prowler/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions.py index 4828752183..2df8d4b751 100644 --- a/prowler/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions.py +++ b/prowler/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions.py @@ -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 = ( diff --git a/prowler/providers/aws/services/securityhub/securityhub_service.py b/prowler/providers/aws/services/securityhub/securityhub_service.py index 5e2d445742..3f7bd31ad6 100644 --- a/prowler/providers/aws/services/securityhub/securityhub_service.py +++ b/prowler/providers/aws/services/securityhub/securityhub_service.py @@ -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}" ) diff --git a/tests/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions_test.py b/tests/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions_test.py index f2acf555d1..f9e903677d 100644 --- a/tests/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions_test.py +++ b/tests/providers/aws/services/config/config_delegated_admin_and_org_aggregator_all_regions/config_delegated_admin_and_org_aggregator_all_regions_test.py @@ -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 ) diff --git a/tests/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py b/tests/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py index c2875af3ed..7a2d434618 100644 --- a/tests/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py +++ b/tests/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py @@ -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 + } diff --git a/tests/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions_test.py b/tests/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions_test.py index 01af147e9a..9ea3003844 100644 --- a/tests/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions_test.py +++ b/tests/providers/aws/services/securityhub/securityhub_delegated_admin_enabled_all_regions/securityhub_delegated_admin_enabled_all_regions_test.py @@ -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 diff --git a/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_fixer_test.py b/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_fixer_test.py index 1d135bda85..ce7ad536f8 100644 --- a/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_fixer_test.py +++ b/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_fixer_test.py @@ -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) diff --git a/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py b/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py index 030aa7fdad..46da053955 100644 --- a/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py +++ b/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py @@ -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 == [] diff --git a/tests/providers/aws/services/securityhub/securityhub_service_test.py b/tests/providers/aws/services/securityhub/securityhub_service_test.py index bdfabe65ef..c636532d5a 100644 --- a/tests/providers/aws/services/securityhub/securityhub_service_test.py +++ b/tests/providers/aws/services/securityhub/securityhub_service_test.py @@ -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 + } diff --git a/tests/providers/aws/utils.py b/tests/providers/aws/utils.py index 0b11798e1f..90e2a98e85 100644 --- a/tests/providers/aws/utils.py +++ b/tests/providers/aws/utils.py @@ -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 From 3b577907e4912bfe397d9892fda8e59e3dbbf84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:23:29 +0200 Subject: [PATCH 4/4] ci(container): gate Grype only on fixable findings, and comment results on the PR (#12341) --- .github/actions/grype-scan/action.yml | 51 +++++++ .github/actions/trivy-scan/action.yml | 35 +++-- .github/scripts/grype-pr-comment.js | 100 +++++++++++++ .github/workflows/api-container-checks.yml | 6 +- .github/workflows/mcp-container-checks.yml | 6 +- .github/workflows/sdk-container-checks.yml | 6 +- .github/workflows/ui-container-checks.yml | 6 +- .grype.yaml | 161 ++------------------- .trivyignore.yaml | 92 ++++++++++++ 9 files changed, 294 insertions(+), 169 deletions(-) create mode 100644 .github/scripts/grype-pr-comment.js diff --git a/.github/actions/grype-scan/action.yml b/.github/actions/grype-scan/action.yml index 1cac3f7674..76d3004c11 100644 --- a/.github/actions/grype-scan/action.yml +++ b/.github/actions/grype-scan/action.yml @@ -18,6 +18,10 @@ inputs: description: 'Upload results to GitHub Security tab' required: false default: 'true' + create-pr-comment: + description: 'Create a comment on the PR with scan results' + required: false + default: 'true' artifact-retention-days: description: 'Days to retain the Grype report artifact' required: false @@ -45,6 +49,7 @@ runs: output-file: 'grype-report.json' fail-build: 'false' by-cve: 'true' # Report CVE ids rather than GHSA, so findings line up with Trivy's + only-fixed: 'true' # A finding with no available fix is not actionable, so it must not gate cache-db: 'true' grype-version: 'v0.116.1' @@ -58,6 +63,7 @@ runs: fail-build: 'false' severity-cutoff: 'high' by-cve: 'true' + only-fixed: 'true' # A finding with no available fix is not actionable, so it must not gate cache-db: 'true' grype-version: 'v0.116.1' @@ -101,6 +107,51 @@ runs: INPUTS_IMAGE_NAME: ${{ inputs.image-name }} INPUTS_IMAGE_TAG: ${{ inputs.image-tag }} + # Before the gate, so the comment is there to explain a failure rather than absent because of it + - name: Comment scan results on PR + if: >- + inputs.create-pr-comment == 'true' + && github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + IMAGE_NAME: ${{ inputs.image-name }} + GITHUB_SHA: ${{ inputs.image-tag }} + CUTOFF: ${{ inputs.fail-on-severity }} + with: + script: | + const comment = require('./.github/scripts/grype-pr-comment.js'); + + // Unique identifier to find our comment + const marker = ``; + const body = marker + '\n' + comment; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(c => c.body?.includes(marker)); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + console.log('✅ Updated existing Grype scan comment'); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + console.log('✅ Created new Grype scan comment'); + } + - name: Check for blocking vulnerabilities if: inputs.fail-on-severity != 'none' shell: bash diff --git a/.github/actions/trivy-scan/action.yml b/.github/actions/trivy-scan/action.yml index 4aa1e47047..b5159767cd 100644 --- a/.github/actions/trivy-scan/action.yml +++ b/.github/actions/trivy-scan/action.yml @@ -14,10 +14,10 @@ inputs: description: 'Severities to scan for (comma-separated)' required: false default: 'CRITICAL,HIGH,MEDIUM,LOW' - fail-on-critical: - description: 'Fail the build if critical vulnerabilities are found' + fail-on-severity: + description: 'Fail the build on findings at this severity or above: critical, high, or none' required: false - default: 'false' + default: 'high' upload-sarif: description: 'Upload results to GitHub Security tab' required: false @@ -62,6 +62,7 @@ runs: severity: ${{ inputs.severity }} exit-code: '0' scanners: 'vuln' + ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate timeout: '5m' version: 'v0.71.2' # Not trivyignores: that input drops the .yaml extension Trivy parses by. @@ -78,6 +79,7 @@ runs: severity: 'CRITICAL,HIGH' exit-code: '0' scanners: 'vuln' + ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate timeout: '5m' version: 'v0.71.2' # Not trivyignores: that input drops the .yaml extension Trivy parses by. @@ -169,13 +171,28 @@ runs: console.log('✅ Created new Trivy scan comment'); } - - name: Check for critical vulnerabilities - if: inputs.fail-on-critical == 'true' && steps.security-check.outputs.critical != '0' + - name: Check for blocking vulnerabilities + if: inputs.fail-on-severity != 'none' shell: bash run: | - echo "::error::Found ${STEPS_SECURITY_CHECK_OUTPUTS_CRITICAL} critical vulnerabilities" - echo "::warning::Please update packages or use a different base image" - exit 1 + if [ "$CUTOFF" = "critical" ]; then + BLOCKING=$CRITICAL + SEVERITIES='["CRITICAL"]' + else + BLOCKING=$((CRITICAL + HIGH)) + SEVERITIES='["CRITICAL","HIGH"]' + fi + if [ "$BLOCKING" -gt 0 ]; then + echo "::error::Found $BLOCKING vulnerabilities at severity ${CUTOFF} or above ($CRITICAL critical, $HIGH high)" + echo "::warning::Update the package, or add it to .trivyignore.yaml with a reason if nothing can be done" + jq -r --argjson severities "$SEVERITIES" \ + '.Results[]?.Vulnerabilities[]? | select(.Severity | IN($severities[])) + | " \(.Severity)\t\(.VulnerabilityID)\t\(.PkgName) \(.InstalledVersion)"' \ + trivy-report.json | sort -u + exit 1 + fi env: - STEPS_SECURITY_CHECK_OUTPUTS_CRITICAL: ${{ steps.security-check.outputs.critical }} + CUTOFF: ${{ inputs.fail-on-severity }} + CRITICAL: ${{ steps.security-check.outputs.critical }} + HIGH: ${{ steps.security-check.outputs.high }} diff --git a/.github/scripts/grype-pr-comment.js b/.github/scripts/grype-pr-comment.js new file mode 100644 index 0000000000..62e81dfddc --- /dev/null +++ b/.github/scripts/grype-pr-comment.js @@ -0,0 +1,100 @@ +const fs = require('fs'); + +// Configuration from environment variables +const REPORT_FILE = process.env.GRYPE_REPORT_FILE || 'grype-report.json'; +const IMAGE_NAME = process.env.IMAGE_NAME || 'container-image'; +const GITHUB_SHA = process.env.GITHUB_SHA || 'unknown'; +const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY || ''; +const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID || ''; +const CUTOFF = process.env.CUTOFF || 'high'; + +// A cutoff of 'critical' blocks only on critical; anything else blocks on high and above +const blocking = CUTOFF === 'critical' ? ['Critical'] : ['Critical', 'High']; + +const report = JSON.parse(fs.readFileSync(REPORT_FILE, 'utf-8')); +const matches = Array.isArray(report.matches) ? report.matches : []; +const ignored = Array.isArray(report.ignoredMatches) ? report.ignoredMatches : []; + +const counts = { Critical: 0, High: 0, Medium: 0, Low: 0, Negligible: 0, Unknown: 0 }; +const blockers = new Map(); + +for (const match of matches) { + const severity = match.vulnerability.severity; + if (counts[severity] !== undefined) { + counts[severity]++; + } + if (blocking.includes(severity)) { + const artifact = match.artifact; + const fixedIn = (match.vulnerability.fix && match.vulnerability.fix.versions || []).join(', '); + // Same CVE can match several install paths of one package; collapse them + blockers.set(`${match.vulnerability.id}|${artifact.name}`, { + id: match.vulnerability.id, + severity, + name: artifact.name, + version: artifact.version, + fixedIn + }); + } +} + +const ignoredBlocking = ignored.filter(m => blocking.includes(m.vulnerability.severity)).length; +const shortSha = GITHUB_SHA.substring(0, 7); +const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; + +const severityConfig = { + Critical: { icon: '🔴', label: 'Critical' }, + High: { icon: '🟠', label: 'High' }, + Medium: { icon: '🟡', label: 'Medium' }, + Low: { icon: '🔵', label: 'Low' } +}; + +let comment = '## 🔎 Container Security Scan (Grype)\n\n'; +comment += `**Image:** \`${IMAGE_NAME}:${shortSha}\`\n`; +comment += `**Last scan:** ${timestamp}\n\n`; + +if (blockers.size === 0) { + comment += '### ✅ Nothing Blocking\n\n'; + comment += `No findings at **${blocking.join(' or ').toLowerCase()}** severity.\n`; +} else { + comment += `### ⚠️ ${blockers.size} Finding(s) Blocking This PR\n\n`; + comment += '| Severity | CVE | Package | Installed | Fixed in |\n'; + comment += '|---|---|---|---|---|\n'; + + const order = { Critical: 0, High: 1 }; + const rows = [...blockers.values()].sort((a, b) => + (order[a.severity] - order[b.severity]) || a.name.localeCompare(b.name)); + + for (const row of rows) { + const config = severityConfig[row.severity]; + comment += `| ${config.icon} ${config.label} | \`${row.id}\` | \`${row.name}\` | ${row.version} | ${row.fixedIn || '—'} |\n`; + } + + comment += '\n**What to do:**\n'; + comment += '- Upgrade the package to the version in the "Fixed in" column.\n'; + comment += '- If it is pinned by another dependency, or the fix is otherwise out of reach, add it to `.grype.yaml` **with the reason**.\n'; + comment += '- Findings with no published fix never appear here: the scan runs with `only-fixed`, so it reports only what can actually be acted on.\n'; +} + +const otherCounts = Object.entries(counts) + .filter(([severity, count]) => !blocking.includes(severity) && count > 0) + .map(([severity, count]) => `${severity.toLowerCase()}: ${count}`); + +if (otherCounts.length > 0) { + comment += `\nNot blocking at this cutoff — ${otherCounts.join(', ')}.\n`; +} + +if (ignoredBlocking > 0) { + comment += `\n${ignoredBlocking} finding(s) excluded by \`.grype.yaml\`, each with a documented reason.\n`; +} + +comment += '\n---\n'; +comment += '📋 **Resources:**\n'; + +if (GITHUB_REPOSITORY && GITHUB_RUN_ID) { + comment += `- [Download full report](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) (see artifacts)\n`; +} + +comment += '- [View in Security tab](https://github.com/' + (GITHUB_REPOSITORY || 'repository') + '/security/code-scanning)\n'; +comment += '- Scanned with [Grype](https://github.com/anchore/grype), alongside Trivy\n'; + +module.exports = comment; diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml index 19e6de3a0f..4694c34e27 100644 --- a/.github/workflows/api-container-checks.yml +++ b/.github/workflows/api-container-checks.yml @@ -84,6 +84,7 @@ jobs: raw.githubusercontent.com:443 objects.githubusercontent.com:443 grype.anchore.io:443 + get.anchore.io:443 debian.map.fastlydns.net:80 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 @@ -113,6 +114,7 @@ jobs: .github/actions/trivy-scan/** .github/actions/grype-scan/** .grype.yaml + .github/scripts/grype-pr-comment.js files_ignore: | api/docs/** api/README.md @@ -150,8 +152,8 @@ jobs: with: image-name: ${{ env.IMAGE_NAME }} image-tag: ${{ github.sha }} - fail-on-critical: 'true' - severity: 'CRITICAL' + fail-on-severity: 'high' + severity: 'CRITICAL,HIGH' - name: Scan container with Grype if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml index a9c19f716d..a2efee62ce 100644 --- a/.github/workflows/mcp-container-checks.yml +++ b/.github/workflows/mcp-container-checks.yml @@ -89,6 +89,7 @@ jobs: objects.githubusercontent.com:443 raw.githubusercontent.com:443 grype.anchore.io:443 + get.anchore.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -105,6 +106,7 @@ jobs: .github/actions/trivy-scan/** .github/actions/grype-scan/** .grype.yaml + .github/scripts/grype-pr-comment.js files_ignore: | mcp_server/README.md mcp_server/CHANGELOG.md @@ -131,8 +133,8 @@ jobs: with: image-name: ${{ env.IMAGE_NAME }} image-tag: ${{ github.sha }} - fail-on-critical: 'true' - severity: 'CRITICAL' + fail-on-severity: 'high' + severity: 'CRITICAL,HIGH' - name: Scan MCP container with Grype if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml index b98207b978..1ec8fbdfb3 100644 --- a/.github/workflows/sdk-container-checks.yml +++ b/.github/workflows/sdk-container-checks.yml @@ -86,6 +86,7 @@ jobs: raw.githubusercontent.com:443 objects.githubusercontent.com:443 grype.anchore.io:443 + get.anchore.io:443 debian.map.fastlydns.net:80 release-assets.githubusercontent.com:443 objects.githubusercontent.com:443 @@ -119,6 +120,7 @@ jobs: .github/actions/trivy-scan/** .github/actions/grype-scan/** .grype.yaml + .github/scripts/grype-pr-comment.js files_ignore: | prowler/CHANGELOG.md prowler/changelog.d/** @@ -145,8 +147,8 @@ jobs: with: image-name: ${{ env.IMAGE_NAME }} image-tag: ${{ github.sha }} - fail-on-critical: 'true' - severity: 'CRITICAL' + fail-on-severity: 'high' + severity: 'CRITICAL,HIGH' - name: Scan SDK container with Grype if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml index 5da65da3f1..1b22b97a8f 100644 --- a/.github/workflows/ui-container-checks.yml +++ b/.github/workflows/ui-container-checks.yml @@ -90,6 +90,7 @@ jobs: objects.githubusercontent.com:443 raw.githubusercontent.com:443 grype.anchore.io:443 + get.anchore.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -106,6 +107,7 @@ jobs: .github/actions/trivy-scan/** .github/actions/grype-scan/** .grype.yaml + .github/scripts/grype-pr-comment.js files_ignore: | ui/CHANGELOG.md ui/changelog.d/** @@ -136,8 +138,8 @@ jobs: with: image-name: ${{ env.IMAGE_NAME }} image-tag: ${{ github.sha }} - fail-on-critical: 'true' - severity: 'CRITICAL' + fail-on-severity: 'high' + severity: 'CRITICAL,HIGH' - name: Scan UI container with Grype if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.grype.yaml b/.grype.yaml index 970fda5319..f6c98b9db7 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -1,154 +1,12 @@ # 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: - # Operating-system packages with no fix available. - # Debian and Alpine have either published no fix or marked these will-not-fix. - # perl-base additionally cannot be removed: the distribution marks it Essential. - - vulnerability: CVE-2026-41992 - package: - name: gzip - - vulnerability: CVE-2026-54369 - package: - name: libacl1 - - vulnerability: CVE-2026-54370 - package: - name: libacl1 - - vulnerability: CVE-2026-5435 - package: - name: libc-bin - - vulnerability: CVE-2026-5450 - package: - name: libc-bin - - vulnerability: CVE-2026-5928 - package: - name: libc-bin - - vulnerability: CVE-2026-5435 - package: - name: libc6 - - vulnerability: CVE-2026-5450 - package: - name: libc6 - - vulnerability: CVE-2026-5928 - package: - name: libc6 - - vulnerability: CVE-2026-10536 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-11856 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-12064 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-8286 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-8924 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-8926 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-8927 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-8932 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-9079 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-9080 - package: - name: libcurl4t64 - - vulnerability: CVE-2026-9545 - package: - name: libcurl4t64 - - vulnerability: CVE-2025-69720 - package: - name: libncursesw6 - - vulnerability: CVE-2026-11822 - package: - name: libsqlite3-0 - - vulnerability: CVE-2026-11824 - package: - name: libsqlite3-0 - - vulnerability: CVE-2026-66032 - package: - name: libssh2-1t64 - - vulnerability: CVE-2026-66033 - package: - name: libssh2-1t64 - - vulnerability: CVE-2026-66034 - package: - name: libssh2-1t64 - - vulnerability: CVE-2026-66035 - package: - name: libssh2-1t64 - - vulnerability: CVE-2025-69720 - package: - name: libtinfo6 - - vulnerability: CVE-2025-69720 - package: - name: ncurses-base - - vulnerability: CVE-2025-69720 - package: - name: ncurses-bin - - vulnerability: CVE-2026-12087 - package: - name: perl-base - - vulnerability: CVE-2026-13221 - package: - name: perl-base - - vulnerability: CVE-2026-42496 - package: - name: perl-base - - vulnerability: CVE-2026-42497 - package: - name: perl-base - - vulnerability: CVE-2026-48959 - package: - name: perl-base - - vulnerability: CVE-2026-48961 - package: - name: perl-base - - vulnerability: CVE-2026-48962 - package: - name: perl-base - - vulnerability: CVE-2026-57432 - package: - name: perl-base - - vulnerability: CVE-2026-57433 - package: - name: perl-base - - vulnerability: CVE-2026-7017 - package: - name: perl-base - - vulnerability: CVE-2026-8376 - package: - name: perl-base - - vulnerability: CVE-2026-9538 - package: - name: perl-base - - vulnerability: CVE-2026-11822 - package: - name: sqlite-libs - - vulnerability: CVE-2026-11824 - package: - name: sqlite-libs - - vulnerability: CVE-2026-58469 - package: - name: wget - - vulnerability: CVE-2026-58471 - package: - name: wget - - vulnerability: CVE-2026-58472 - package: - name: wget - # 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 @@ -167,18 +25,17 @@ ignore: package: name: Microsoft.Bcl.Memory - # cryptography is capped below 49 by alibabacloud-tea-openapi==0.4.5, - # which requires cryptography>=3.0.0,<49.0.0. The fix is in 50.0.0, so it is - # unreachable until that dependency raises its ceiling. + # cryptography is capped below 49 by alibabacloud-tea-openapi==0.4.5, which requires + # cryptography>=3.0.0,<49.0.0. The fix is in 50.0.0, so it is unreachable until that + # dependency raises its ceiling. Verified by attempting the upgrade: resolution fails. - vulnerability: CVE-2026-69247 package: name: cryptography # The CPython interpreter, compiled into the official base image. - # TEMPORARY, unlike the entries above: these are reachable by moving to Python 3.13, - # which is a runtime upgrade pending its own evaluation. Three of them (CVE-2026-11940, - # CVE-2026-11972, CVE-2026-15308) need 3.15 and remain unfixable either way -- the MCP - # image already runs 3.13.14 and still reports them. + # 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 diff --git a/.trivyignore.yaml b/.trivyignore.yaml index f7f39e292c..da7c123015 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -9,6 +9,11 @@ # # `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 @@ -61,3 +66,90 @@ vulnerabilities: 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 + + # cryptography is held below 49 by alibabacloud-tea-openapi==0.4.5, which requires + # cryptography>=3.0.0,<49.0.0. The fixes are in 49.0.0 and 50.0.0, so both sit above the + # ceiling. Verified by attempting the upgrade: resolution fails. Reachable again only when + # that dependency raises its bound. + - id: CVE-2026-69247 + purls: + - "pkg:pypi/cryptography" + expired_at: 2026-11-30 + - id: CVE-2026-69249 + purls: + - "pkg:pypi/cryptography" + expired_at: 2026-11-30